diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index a15a79ab..607ae973 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -25,7 +25,7 @@ export default function AppShell({ data-app-shell className="flex h-screen flex-col overflow-hidden bg-background text-foreground" > -
+
(null); - const { uploadFiles } = useWorkspaceFileIntake(); + const { requestFileUpload, uploadFiles } = useWorkspaceFileIntake(); + const viewCapabilities = useWorkspaceViewCapabilities(); const parentId = getWorkspaceBrowseParentId(activeItem); const children = getWorkspaceChildren(items, parentId); const { folders, items: nonFolderItems } = splitWorkspaceChildren(children); const isWorkspaceRoot = parentId === null; const isEmpty = children.length === 0; + const requestUploadToBrowseLocation = () => requestFileUpload(parentId); const handleNativeFileDrop = (files: FileList) => { if (!capabilities.canMutateContent) { return; @@ -195,60 +199,73 @@ function WorkspaceBrowseContent({ stopPropagation: false, }); + const browseSurfaceContent = ( + <> + {folders.length > 0 ? ( + + ) : null} + {nonFolderItems.length > 0 ? ( + + ) : null} + {isEmpty ? ( + isWorkspaceRoot && capabilities.canMutateContent ? ( + + ) : ( + + ) + ) : null} + + ); + const browseSurface = ( +
+ {browseSurfaceContent} +
+ ); + return ( <>
- - - } - > - {folders.length > 0 ? ( - - ) : null} - {nonFolderItems.length > 0 ? ( - - ) : null} - {isEmpty ? ( - isWorkspaceRoot && capabilities.canMutateContent ? ( - - ) : ( - - ) - ) : null} - - - - - + {viewCapabilities.contextMenus ? ( + + + + + + + ) : ( + browseSurface + )} - - - - - This folder is empty - Items added here will appear in this folder. - - - ); - } - - return ( - +function WorkspaceBrowseEmptyState({ + canUpload, + isWorkspaceRoot, + onUploadFiles, +}: { + canUpload: boolean; + isWorkspaceRoot: boolean; + onUploadFiles: () => void; +}) { + const uploadLabel = isWorkspaceRoot + ? "Upload files to this workspace" + : "Upload files to this folder"; + const Icon = isWorkspaceRoot ? Eye : FolderOpen; + const title = isWorkspaceRoot ? "This workspace is empty" : "This folder is empty"; + const description = canUpload + ? "Click anywhere here to upload files." + : isWorkspaceRoot + ? "An editor needs to add the first items before anything appears here." + : "Items added here will appear in this folder."; + const emptyState = ( + - + - This workspace is empty - - An editor needs to add the first items before anything appears here. - + {title} + {description} ); + + if (canUpload) { + return ( + + {emptyState} + + ); + } + + return emptyState; } function WorkspaceItemGrid({ @@ -445,6 +483,8 @@ function WorkspaceItemView({ onRenameItem: (item: WorkspaceItem) => void; onDeleteItem: (item: WorkspaceItem) => void; }) { + const viewCapabilities = useWorkspaceViewCapabilities(); + if (item.type === "document") { return ( @@ -465,31 +505,33 @@ function WorkspaceItemView({ } const { Icon: ItemIcon, iconClassName, surfaceClassName } = getWorkspaceItemDisplay(item); + const itemViewContent = ( +
+
+ ); + const itemViewSurface = ( +
+ {itemViewContent} +
+ ); + + if (!viewCapabilities.contextMenus) { + return
{itemViewSurface}
; + } return (
- - } - > -
-
-
+ void; }; @@ -36,18 +36,20 @@ export function WorkspaceFileToolbar({ return ( - - - Capture - + {capture ? ( + + + Capture + + ) : null} }> diff --git a/src/features/workspaces/components/WorkspaceFileViewer.tsx b/src/features/workspaces/components/WorkspaceFileViewer.tsx index 58ae2e94..51855089 100644 --- a/src/features/workspaces/components/WorkspaceFileViewer.tsx +++ b/src/features/workspaces/components/WorkspaceFileViewer.tsx @@ -3,6 +3,7 @@ import { type ComponentType, type LazyExoticComponent, lazy, Suspense } from "re import { ContextMenu, ContextMenuTrigger } from "#/components/ui/context-menu"; import { Spinner } from "#/components/ui/spinner"; import { WorkspaceItemActionsContextMenuContent } from "#/features/workspaces/components/WorkspaceItemActionsMenu"; +import { useWorkspaceViewCapabilities } from "#/features/workspaces/components/workspace-view-policy"; import { getWorkspaceItemDisplay } from "#/features/workspaces/model/item-display"; import type { WorkspaceItem } from "#/features/workspaces/model/types"; import { @@ -44,20 +45,26 @@ export default function WorkspaceFileViewer({ }: WorkspaceFileViewerProps) { const descriptor = resolveWorkspaceFileTypeFromItem(item); const Viewer = descriptor ? workspaceFileViewers[descriptor.assetKind] : null; + const viewCapabilities = useWorkspaceViewCapabilities(); + const viewerContent = Viewer ? ( + }> + + + ) : ( +
+ +
+ ); + + if (!viewCapabilities.contextMenus) { + return
{viewerContent}
; + } return (
}> - {Viewer ? ( - }> - - - ) : ( -
- -
- )} + {viewerContent}
-
+
+
)} - {status === "ready" ? ( + {enableFileCapture && status === "ready" ? ( ) : null} - - setIsCaptureActive(false)} - onToggle={() => setIsCaptureActive((current) => !current)} - /> + {enableFileCapture ? ( + <> + + + + ) : null}
); } diff --git a/src/features/workspaces/components/WorkspaceItemCard.tsx b/src/features/workspaces/components/WorkspaceItemCard.tsx index c8f1d16a..373d272f 100644 --- a/src/features/workspaces/components/WorkspaceItemCard.tsx +++ b/src/features/workspaces/components/WorkspaceItemCard.tsx @@ -12,11 +12,14 @@ import { workspaceItemCardHoverClass, workspaceItemCardSelectedClass, workspaceItemCardUnselectedHoverClass, + workspaceItemPreviewControlsLayerClass, } from "#/features/workspaces/components/workspace-item-card-chrome"; import { WorkspaceItemCardFooter } from "#/features/workspaces/components/workspace-item-card-footer"; +import { WorkspaceItemCardPreviewControls } from "#/features/workspaces/components/workspace-item-card-preview-controls"; import { WorkspaceItemCardPreviewStage } from "#/features/workspaces/components/workspace-item-card-preview-stage"; import { useWorkspaceMutationAccess } from "#/features/workspaces/components/workspace-mutation-access"; import { workspaceItemSortablePlugins } from "#/features/workspaces/components/workspace-sortable-plugins"; +import { useWorkspaceViewCapabilities } from "#/features/workspaces/components/workspace-view-policy"; import { createWorkspaceItemDragData, getWorkspaceDragSource, @@ -58,6 +61,7 @@ export default function WorkspaceItemCard({ onElementChange, }: WorkspaceItemCardProps) { const { capabilities, itemSortableDisabled } = useWorkspaceMutationAccess(); + const viewCapabilities = useWorkspaceViewCapabilities(); const { canMutateContent } = capabilities; const isFolder = item.type === "folder"; const row = isFolder ? "folder" : "item"; @@ -206,14 +210,17 @@ export default function WorkspaceItemCard({ aria-label={`Open ${item.name}`} onClick={handleOpen} /> - + +
+ +
{showFolderDropAffordance ? (
) : null} - + - {canMutateContent ? ( - - ) : ( - {item.name} - )} + {isFolder ? ( meta ? ( @@ -251,6 +251,10 @@ export default function WorkspaceItemCard({ ); + if (!viewCapabilities.contextMenus) { + return card; + } + return ( @@ -263,3 +267,31 @@ export default function WorkspaceItemCard({ ); } + +function WorkspaceItemCardTitle({ + canRename, + itemName, + onRenameClick, +}: { + canRename: boolean; + itemName: string; + onRenameClick: (event: MouseEvent) => void; +}) { + if (!canRename) { + return {itemName}; + } + + return ( + <> + {itemName} + + + ); +} diff --git a/src/features/workspaces/components/WorkspaceItemPreviewSurface.tsx b/src/features/workspaces/components/WorkspaceItemPreviewSurface.tsx index 0486bc65..c71c12db 100644 --- a/src/features/workspaces/components/WorkspaceItemPreviewSurface.tsx +++ b/src/features/workspaces/components/WorkspaceItemPreviewSurface.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { workspaceItemDocumentPreviewPanelClass, @@ -32,17 +32,18 @@ export default function WorkspaceItemPreviewSurface({ item }: WorkspaceItemPrevi function WorkspaceItemDocumentPreview({ item }: { item: WorkspaceItem }) { const previewText = getWorkspaceDocumentPreviewText(item); + const desktopPreview = previewText ? ( +
+

{previewText}

+
+ ) : ( + + ); return ( -
- {previewText ? ( -
-

{previewText}

-
- ) : ( - - )} -
+ + {desktopPreview} + ); } @@ -65,7 +66,7 @@ function WorkspaceItemFilePreview({ item }: { item: WorkspaceItem }) { if (showImage) { return ( -
+ setFailedPreviewUrl(previewUrl)} /> -
+ ); } return ; } +function WorkspaceItemMobileIconDesktopPreview({ + children, + item, +}: { + children: ReactNode; + item: WorkspaceItem; +}) { + return ( + <> +
+ +
+
{children}
+ + ); +} + /** Icon-only preview for folders and types without a custom thumbnail yet. */ function WorkspaceItemIconPreview({ item }: { item: WorkspaceItem }) { const { Icon, iconClassName } = getWorkspaceItemDisplay(item); diff --git a/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx b/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx index b517c214..319a48e6 100644 --- a/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx +++ b/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx @@ -20,7 +20,7 @@ type WorkspaceItemToolbarRegistration = slotId: string; } | { - capture: { + capture?: { isActive: boolean; onToggle: () => void; }; @@ -96,7 +96,7 @@ export function useFileItemToolbar({ fileUrl, slotId, }: { - capture: { + capture?: { isActive: boolean; onToggle: () => void; }; @@ -106,14 +106,22 @@ export function useFileItemToolbar({ }) { const context = use(WorkspaceItemToolbarContext); const setRegistration = context?.setRegistration; + const captureIsActive = capture?.isActive; + const captureOnToggle = capture?.onToggle; useEffect(() => { if (!setRegistration) { return; } + const registeredCapture = captureOnToggle + ? { + isActive: Boolean(captureIsActive), + onToggle: captureOnToggle, + } + : undefined; const registration = { - capture, + capture: registeredCapture, fileName, fileUrl, kind: "file" as const, @@ -125,7 +133,8 @@ export function useFileItemToolbar({ existing?.kind === "file" && existing.fileName === fileName && existing.fileUrl === fileUrl && - existing.capture.isActive === capture.isActive + existing.capture?.isActive === registeredCapture?.isActive && + existing.capture?.onToggle === registeredCapture?.onToggle ) { return current; } @@ -148,7 +157,7 @@ export function useFileItemToolbar({ return next; }); }; - }, [capture, fileName, fileUrl, slotId, setRegistration]); + }, [captureIsActive, captureOnToggle, fileName, fileUrl, slotId, setRegistration]); } export function WorkspaceItemToolbarSlot({ diff --git a/src/features/workspaces/components/WorkspaceLayout.tsx b/src/features/workspaces/components/WorkspaceLayout.tsx index 2bb0afdf..9894feeb 100644 --- a/src/features/workspaces/components/WorkspaceLayout.tsx +++ b/src/features/workspaces/components/WorkspaceLayout.tsx @@ -9,6 +9,7 @@ import WorkspaceDragProvider from "#/features/workspaces/components/WorkspaceDra import { WorkspaceFileIntakeProvider } from "#/features/workspaces/components/WorkspaceFileIntakeProvider"; import { WorkspaceFileUploadProvider } from "#/features/workspaces/components/WorkspaceFileUploadProvider"; import { WorkspaceItemToolbarProvider } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; +import WorkspaceMobileLayout from "#/features/workspaces/components/WorkspaceMobileLayout"; import WorkspacePaneRenderer from "#/features/workspaces/components/WorkspacePaneRenderer"; import { WorkspacePdfEngineProvider } from "#/features/workspaces/components/WorkspacePdfEngineProvider"; import { WorkspaceMaximizedPresentation } from "#/features/workspaces/components/WorkspacePresentation"; @@ -17,6 +18,10 @@ import WorkspaceSplitPresentation from "#/features/workspaces/components/Workspa import WorkspaceStandardTabPanes from "#/features/workspaces/components/WorkspaceStandardTabPanes"; import WorkspaceTopBar from "#/features/workspaces/components/WorkspaceTopBar"; import { WorkspaceMutationAccessProvider } from "#/features/workspaces/components/workspace-mutation-access"; +import { + useWorkspaceViewPolicy, + WorkspaceViewCapabilitiesProvider, +} from "#/features/workspaces/components/workspace-view-policy"; import type { WorkspaceItemType, WorkspaceSummary } from "#/features/workspaces/contracts"; import type { WorkspaceItem } from "#/features/workspaces/model/types"; import { isWorkspaceItemView } from "#/features/workspaces/model/view"; @@ -111,8 +116,10 @@ export function WorkspaceShell({ activeViewFromUrl, }); const normalizedUiSession = useWorkspaceUiSession(workspace.id); + const { capabilities: viewCapabilities, viewportMode } = useWorkspaceViewPolicy(); const selectedItemIds = useWorkspaceSelectionItemIds(workspace.id); const { chatSurfaceMode, presentation } = normalizedUiSession; + const mobileChatSurfaceMode = chatSurfaceMode === "docked" ? "hidden" : chatSurfaceMode; const hasHeavyViewerRuntimeItems = scopedItems.some(workspaceItemRequiresHeavyViewerRuntime); const createWorkspaceItem = (input: { type: WorkspaceItemType; parentId: string | null }) => { if (!getWorkspaceMemberCapabilities(workspace.membershipRole).canMutateContent) { @@ -175,8 +182,44 @@ export function WorkspaceShell({ workspaceName: workspace.name, }; + const contextBar = ( + + ); + const standardTabPanes = ( + + ); + const presentationContent = - presentation.mode === "maximized" ? ( + viewportMode === "mobile" ? ( + + setChatSurfaceMode(workspace.id, "fullscreen")} + chatPanel={} + /> + + ) : presentation.mode === "maximized" ? ( - } + contextBar={contextBar} presence={realtime} onActivateTab={activateWorkspaceTab} onCloseTab={closeWorkspaceTab} @@ -232,16 +264,7 @@ export function WorkspaceShell({ onOpenItem={openItem} /> ) : ( - + standardTabPanes ) } chatPanel={} @@ -258,7 +281,9 @@ export function WorkspaceShell({ onMoveItems={moveWorkspaceItemsMutation.mutate} onWorkspaceDragCommand={dispatchWorkspaceDragCommand} > - {presentationContent} + + {presentationContent} + diff --git a/src/features/workspaces/components/WorkspaceMobileLayout.tsx b/src/features/workspaces/components/WorkspaceMobileLayout.tsx new file mode 100644 index 00000000..37ab47f8 --- /dev/null +++ b/src/features/workspaces/components/WorkspaceMobileLayout.tsx @@ -0,0 +1,91 @@ +import { Link } from "@tanstack/react-router"; +import { MessageSquare, Share2 } from "lucide-react"; +import { type ReactElement, type ReactNode, useState } from "react"; + +import ThinkExLogo from "#/components/ThinkExLogo"; +import UserProfileDropdown from "#/components/UserProfileDropdown"; +import WorkspaceFrame from "#/features/workspaces/components/WorkspaceFrame"; +import { WorkspaceShareDialog } from "#/features/workspaces/components/WorkspaceShareDialog"; +import { + WorkspaceToolbarIconButton, + WorkspaceToolbarTextButton, +} from "#/features/workspaces/components/WorkspaceToolbar"; +import type { WorkspaceSummary } from "#/features/workspaces/contracts"; +import type { WorkspaceAiChatSurfaceMode } from "#/features/workspaces/state/workspace-ui-store"; + +interface WorkspaceMobileLayoutProps { + workspace: WorkspaceSummary; + contextBar: ReactNode; + content: ReactNode; + chatPanel?: ReactElement; + chatSurfaceMode: WorkspaceAiChatSurfaceMode; + onOpenChat: () => void; +} + +export default function WorkspaceMobileLayout({ + workspace, + contextBar, + content, + chatPanel, + chatSurfaceMode, + onOpenChat, +}: WorkspaceMobileLayoutProps) { + const [shareOpen, setShareOpen] = useState(false); + const isChatOpen = chatSurfaceMode !== "hidden"; + + return ( +
+ +
+ + + + + +
+ {contextBar} +
+ } + content={content} + /> + + + {chatPanel && isChatOpen ? ( +
{chatPanel}
+ ) : null} + + ); +} diff --git a/src/features/workspaces/components/WorkspacePdfViewer.tsx b/src/features/workspaces/components/WorkspacePdfViewer.tsx index f8b4c84e..5540bd5f 100644 --- a/src/features/workspaces/components/WorkspacePdfViewer.tsx +++ b/src/features/workspaces/components/WorkspacePdfViewer.tsx @@ -37,7 +37,7 @@ import { import { TilingLayer, TilingPluginPackage } from "@embedpdf/plugin-tiling/react"; import { Viewport, ViewportPluginPackage } from "@embedpdf/plugin-viewport/react"; import { ZoomGestureWrapper, ZoomMode, ZoomPluginPackage } from "@embedpdf/plugin-zoom/react"; -import { type ReactNode, useEffect, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useState } from "react"; import { Spinner } from "#/components/ui/spinner"; import { WorkspaceCaptureShortcuts, @@ -52,6 +52,7 @@ import { type WorkspacePdfCaptureResult, } from "#/features/workspaces/components/WorkspacePdfCapture"; import { WorkspacePdfPageControl } from "#/features/workspaces/components/WorkspacePdfPageControl"; +import { useWorkspaceViewCapabilities } from "#/features/workspaces/components/workspace-view-policy"; import { createCaptureAttachmentFile } from "#/features/workspaces/components/workspace-region-capture"; import { stageCaptureAttachmentToComposerWithFeedback } from "#/features/workspaces/composer/workspace-composer-actions"; import type { WorkspaceItem } from "#/features/workspaces/model/types"; @@ -105,12 +106,24 @@ export default function WorkspacePdfViewer({ const fileUrl = getWorkspaceFileContentUrl(workspaceId, item.id); const { engine, error, isLoading } = useEngineContext(); const [isCaptureActive, setIsCaptureActive] = useState(false); + const viewCapabilities = useWorkspaceViewCapabilities(); + const enableFileCapture = viewCapabilities.fileCapture; + const enableSelectionMenu = viewCapabilities.contextMenus; + const captureActive = enableFileCapture && isCaptureActive; + const exitCapture = useCallback(() => { + setIsCaptureActive(false); + }, []); + const toggleCapture = useCallback(() => { + setIsCaptureActive((current) => !current); + }, []); useFileItemToolbar({ - capture: { - isActive: isCaptureActive, - onToggle: () => setIsCaptureActive((current) => !current), - }, + capture: enableFileCapture + ? { + isActive: captureActive, + onToggle: toggleCapture, + } + : undefined, fileName: item.name, fileUrl, slotId: toolbarSlotId ?? item.id, @@ -133,10 +146,12 @@ export default function WorkspacePdfViewer({ documentId={item.id} fileName={item.name} fileUrl={fileUrl} - isCaptureActive={isCaptureActive} + isCaptureActive={captureActive} itemId={item.id} - onCaptureModeExit={() => setIsCaptureActive(false)} - onCaptureModeToggle={() => setIsCaptureActive((current) => !current)} + onCaptureModeExit={exitCapture} + onCaptureModeToggle={toggleCapture} + enableFileCapture={enableFileCapture} + enableSelectionMenu={enableSelectionMenu} workspaceId={workspaceId} /> ) : ( @@ -145,7 +160,7 @@ export default function WorkspacePdfViewer({ } )} - + {enableFileCapture ? : null} ); } @@ -153,6 +168,8 @@ export default function WorkspacePdfViewer({ function WorkspacePdfDocumentLoader({ activeDocumentId, documentId, + enableFileCapture, + enableSelectionMenu, fileName, fileUrl, isCaptureActive, @@ -163,6 +180,8 @@ function WorkspacePdfDocumentLoader({ }: { activeDocumentId: string | null; documentId: string; + enableFileCapture: boolean; + enableSelectionMenu: boolean; fileName: string; fileUrl: string; isCaptureActive: boolean; @@ -245,6 +264,8 @@ function WorkspacePdfDocumentLoader({ itemId={itemId} onCaptureModeExit={onCaptureModeExit} onCaptureModeToggle={onCaptureModeToggle} + enableFileCapture={enableFileCapture} + enableSelectionMenu={enableSelectionMenu} workspaceId={workspaceId} {...props} /> @@ -256,6 +277,8 @@ function WorkspacePdfDocumentLoader({ function WorkspacePdfDocumentContent({ documentId, documentState, + enableFileCapture, + enableSelectionMenu, fileName, fileUrl, isCaptureActive, @@ -269,6 +292,8 @@ function WorkspacePdfDocumentContent({ }: { documentId: string; documentState: DocumentState; + enableFileCapture: boolean; + enableSelectionMenu: boolean; fileName: string; fileUrl: string; isCaptureActive: boolean; @@ -318,12 +343,16 @@ function WorkspacePdfDocumentContent({ workspaceId={workspaceId} /> - - + {enableFileCapture ? ( + <> + + + + ) : null} )} @@ -358,6 +389,8 @@ function WorkspacePdfDocumentContent({ function WorkspacePdfPage({ documentId, documentState, + enableFileCapture, + enableSelectionMenu, isCaptureActive, itemId, onCapture, @@ -368,6 +401,8 @@ function WorkspacePdfPage({ }: { documentId: string; documentState: DocumentState; + enableFileCapture: boolean; + enableSelectionMenu: boolean; isCaptureActive: boolean; itemId: string; onCapture: (capture: WorkspacePdfCaptureResult) => void; @@ -398,15 +433,19 @@ function WorkspacePdfPage({ ( - - )} + selectionMenu={ + enableSelectionMenu + ? (props) => ( + + ) + : undefined + } textStyle={{ background: "var(--selection)", }} @@ -416,7 +455,7 @@ function WorkspacePdfPage({ documentId={documentId} pageIndex={pageLayout.pageIndex} /> - {page ? ( + {enableFileCapture && page ? ( void }) { return (
-
+ -
+
); } @@ -107,7 +112,7 @@ function WorkspaceRootDropEmptyState() { Drop your files here - Or click New to get started + Click anywhere here to upload files ); diff --git a/src/features/workspaces/components/WorkspaceUploadClickTarget.tsx b/src/features/workspaces/components/WorkspaceUploadClickTarget.tsx new file mode 100644 index 00000000..9c561fe2 --- /dev/null +++ b/src/features/workspaces/components/WorkspaceUploadClickTarget.tsx @@ -0,0 +1,29 @@ +import type { ReactNode } from "react"; + +import { cn } from "#/lib/utils"; + +interface WorkspaceUploadClickTargetProps { + "aria-label": string; + children: ReactNode; + className?: string; + onUploadFiles: () => void; +} + +export function WorkspaceUploadClickTarget({ + "aria-label": ariaLabel, + children, + className, + onUploadFiles, +}: WorkspaceUploadClickTargetProps) { + return ( +
+
+ ); +} diff --git a/src/features/workspaces/components/ai-chat/AiChatPanelToolbar.tsx b/src/features/workspaces/components/ai-chat/AiChatPanelToolbar.tsx index 97a4b81b..3bc22f92 100644 --- a/src/features/workspaces/components/ai-chat/AiChatPanelToolbar.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatPanelToolbar.tsx @@ -142,6 +142,7 @@ export default function AiChatPanelToolbar({ {isMaximized ? : } diff --git a/src/features/workspaces/components/workspace-item-card-chrome.ts b/src/features/workspaces/components/workspace-item-card-chrome.ts index de04f11b..da9288ad 100644 --- a/src/features/workspaces/components/workspace-item-card-chrome.ts +++ b/src/features/workspaces/components/workspace-item-card-chrome.ts @@ -1,19 +1,19 @@ /** Shared layout + appearance tokens for workspace item cards. */ export const workspaceItemPreviewControlClass = - "relative z-20 rounded-[4px] border border-border/80 bg-card/95 text-muted-foreground shadow-none backdrop-blur-md transition-[background-color,border-color,color,opacity] hover:border-foreground/30 hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 dark:border-white/15 dark:bg-card/90 dark:text-muted-foreground dark:hover:border-white/35 dark:hover:bg-secondary dark:hover:text-foreground/95 data-popup-open:border-foreground/30 data-popup-open:bg-secondary data-popup-open:text-foreground dark:data-popup-open:border-white/35 dark:data-popup-open:bg-secondary dark:data-popup-open:text-foreground/95"; + "relative z-20 size-9 rounded-[6px] border border-border/80 bg-card/95 text-muted-foreground shadow-none backdrop-blur-md transition-[background-color,border-color,color,opacity] hover:border-foreground/30 hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 dark:border-white/15 dark:bg-card/90 dark:text-muted-foreground dark:hover:border-white/35 dark:hover:bg-secondary dark:hover:text-foreground/95 data-popup-open:border-foreground/30 data-popup-open:bg-secondary data-popup-open:text-foreground dark:data-popup-open:border-white/35 dark:data-popup-open:bg-secondary dark:data-popup-open:text-foreground/95 sm:size-6 sm:rounded-[4px]"; export const workspaceItemPreviewControlOverlayClass = - "pointer-events-none opacity-0 transition-opacity group-hover/item:pointer-events-auto group-hover/item:opacity-100 data-popup-open:pointer-events-auto data-popup-open:opacity-100"; + "pointer-events-auto opacity-100 transition-opacity sm:pointer-events-none sm:opacity-0 sm:group-hover/item:pointer-events-auto sm:group-hover/item:opacity-100 sm:data-popup-open:pointer-events-auto sm:data-popup-open:opacity-100"; export const workspaceItemPreviewControlSelectedClass = "pointer-events-auto border-info bg-info text-white opacity-100 dark:border-info dark:bg-info dark:text-white"; export const workspaceItemPreviewControlRowClass = - "relative z-10 flex h-10 items-center justify-between px-2"; + "relative z-10 flex items-center justify-end gap-2 sm:h-10 sm:justify-between sm:gap-1 sm:px-2"; export const workspaceItemCardBaseClass = - "workspace-item-card group/item relative flex h-full min-h-44 cursor-pointer flex-col gap-0 overflow-hidden py-0 transition-[background-color,box-shadow] active:cursor-grabbing"; + "workspace-item-card group/item relative flex h-full min-h-20 cursor-pointer flex-row gap-0 overflow-hidden py-0 transition-[background-color,box-shadow] active:cursor-grabbing sm:min-h-44 sm:flex-col"; export const workspaceItemCardHoverClass = "hover:bg-secondary dark:hover:bg-accent/75"; @@ -23,18 +23,20 @@ export const workspaceItemCardUnselectedHoverClass = export const workspaceItemCardSelectedClass = "data-[selected=true]:ring-2 data-[selected=true]:ring-info"; -export const workspaceItemGridClass = "grid grid-cols-[repeat(auto-fill,minmax(13rem,1fr))] gap-5"; +export const workspaceItemGridClass = + "grid grid-cols-1 gap-3 sm:grid-cols-[repeat(auto-fill,minmax(13rem,1fr))] sm:gap-5"; export const workspaceItemPreviewStageClass = - "pointer-events-none relative z-10 min-h-20 flex-1 overflow-hidden bg-muted"; + "pointer-events-none relative z-10 w-14 shrink-0 overflow-hidden bg-muted sm:min-h-20 sm:w-auto sm:flex-1"; export const workspaceItemPreviewContentLayerClass = "absolute inset-0"; -export const workspaceItemPreviewControlsLayerClass = "absolute inset-x-0 top-0 z-20"; +export const workspaceItemPreviewControlsLayerClass = + "pointer-events-none absolute top-1/2 right-2 z-20 -translate-y-1/2 sm:inset-x-0 sm:top-0 sm:translate-y-0"; export const workspaceItemDocumentPreviewPanelClass = "size-full overflow-hidden p-3"; export const workspaceItemDocumentPreviewTextClass = "size-full overflow-hidden break-words whitespace-pre-line text-[11px] leading-[1.45] text-muted-foreground/70 line-clamp-[11]"; -export const workspaceItemPreviewIconClass = "size-10"; +export const workspaceItemPreviewIconClass = "size-8 sm:size-10"; diff --git a/src/features/workspaces/components/workspace-item-card-footer.tsx b/src/features/workspaces/components/workspace-item-card-footer.tsx index 25a6aa0a..df8ae5cf 100644 --- a/src/features/workspaces/components/workspace-item-card-footer.tsx +++ b/src/features/workspaces/components/workspace-item-card-footer.tsx @@ -14,9 +14,9 @@ export function WorkspaceItemCardFooter({ item }: WorkspaceItemCardFooterProps) return ( +