Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ export const ImagePill = React.forwardRef<
{
context: ImageMessageContext;
onRemove: () => void;
onSave?: () => void;
}
>(({ context, onRemove }, ref) => {
>(({ context, onRemove, onSave }, ref) => {
if (context.type !== MessageContextType.IMAGE) {
console.warn('ImagePill received non-image context');
return null;
}

const isVideo = isVideoFile(context.mimeType);
const canSave = context.source === 'external' && !!onSave;

return (
<motion.span
Expand All @@ -36,7 +38,6 @@ export const ImagePill = React.forwardRef<
key={context.displayName}
ref={ref}
>
{/* Left side: Image/Video thumbnail */}
<div className="w-7 h-7 flex items-center justify-center overflow-hidden relative">
{isVideo ? (
<video
Expand All @@ -55,12 +56,23 @@ export const ImagePill = React.forwardRef<
<div className="absolute inset-0 border-l-[1px] border-y-[1px] rounded-l-md border-white/10 pointer-events-none" />
</div>

{/* Right side: Filename */}
<span className="text-xs overflow-hidden whitespace-nowrap text-ellipsis max-w-[100px] pr-1">
{getTruncatedName(context)}
</span>

{/* Hover X button */}
{canSave && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSave();
}}
className="absolute -top-1.5 -left-1.5 w-6 h-6 p-1 rounded-full bg-primary flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer"
>
<Icons.Download className="w-2.5 h-2.5 text-primary-foreground" />
</button>
)}

<button
onClick={(e) => {
e.preventDefault();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEditorEngine } from '@/components/store/editor';
import { DefaultSettings } from '@onlook/constants';
import type { ImageMessageContext, MessageContext } from '@onlook/models/chat';
import { MessageContextType } from '@onlook/models/chat';
import { assertNever } from '@onlook/utility';
import { toast } from '@onlook/ui/sonner';
import { assertNever, sanitizeFilename } from '@onlook/utility';
import { observer } from 'mobx-react-lite';
import { AnimatePresence } from 'motion/react';
import { useMemo } from 'react';
Expand All @@ -17,6 +19,31 @@ const typeOrder = {
[MessageContextType.IMAGE]: 5,
};

const mimeExtensionMap: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
'image/gif': 'gif',
'image/svg+xml': 'svg',
'image/bmp': 'bmp',
'image/x-icon': 'ico',
'video/mp4': 'mp4',
'video/webm': 'webm',
};

const getExtensionFromMimeType = (mimeType: string) => {
return mimeExtensionMap[mimeType] ?? 'png';
};

const getSanitizedFileName = (displayName: string, mimeType: string) => {
const sanitizedName = sanitizeFilename(displayName || `chat-image-${Date.now()}`);
if (sanitizedName.includes('.')) {
return sanitizedName;
}
const extension = getExtensionFromMimeType(mimeType);
return `${sanitizedName}.${extension}`;
};

const getStableKey = (context: MessageContext, index: number): string => {
switch (context.type) {
case MessageContextType.FILE:
Expand Down Expand Up @@ -46,6 +73,55 @@ export const InputContextPills = observer(() => {
editorEngine.chat.context.context = newContext;
};

const handleSaveImageContext = async (imageContext: ImageMessageContext) => {
if (imageContext.source === 'local' && imageContext.path) {
toast.success('Image already exists in project assets');
return;
}

try {
const destinationFolder = `${DefaultSettings.IMAGE_FOLDER}/images`;
const fileName = getSanitizedFileName(imageContext.displayName, imageContext.mimeType);

let targetPath = `${destinationFolder}/${fileName}`;
let counter = 1;

while (await editorEngine.activeSandbox.fileExists(targetPath)) {
const lastDotIndex = fileName.lastIndexOf('.');
const baseName =
lastDotIndex > 0 ? fileName.slice(0, lastDotIndex) : fileName;
const extension =
lastDotIndex > 0 ? fileName.slice(lastDotIndex) : '';
targetPath = `${destinationFolder}/${baseName}-${counter}${extension}`;
counter += 1;
}

const imageResponse = await fetch(imageContext.content);
const imageBuffer = await imageResponse.arrayBuffer();
const imageData = new Uint8Array(imageBuffer);

await editorEngine.activeSandbox.writeFile(targetPath, imageData);

const savedImageName = targetPath.split('/').pop() ?? fileName;
const updatedImageContext: ImageMessageContext = {
...imageContext,
source: 'local',
path: targetPath,
branchId: editorEngine.branches.activeBranch.id,
Comment on lines +89 to +110
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether activeSandbox and branches.activeBranch are guaranteed to refer to the same branch.
ast-grep --pattern $'get activeSandbox() {
  $$$
}'
rg -nP --type=ts -C3 '\bactiveSandbox\b' -g '!**/node_modules/**'
rg -nP --type=ts -C3 '\bactiveBranch\b' -g '!**/node_modules/**'

Repository: onlook-dev/onlook

Length of output: 50373


🏁 Script executed:

rg -nP --type=ts 'activeBranchData\s*=' | head -30

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

rg -nP --type=ts 'setActiveBranch|switchBranch|activeBranch\s*=' -A 3 | head -50

Repository: onlook-dev/onlook

Length of output: 2001


🏁 Script executed:

ast-grep --pattern 'switchBranch($$$) { $$$ }' | head -100

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

rg -nP --type=ts 'activeBranchData' -B 2 -A 2 | head -80

Repository: onlook-dev/onlook

Length of output: 3604


🏁 Script executed:

rg -nP --type=ts 'private.*activeBranchData|this\.activeBranchData\s*=' -A 2 | head -60

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

rg -nP --type=ts 'currentBranchId\s*=' -B 3 -A 3 | head -100

Repository: onlook-dev/onlook

Length of output: 4394


🏁 Script executed:

# Search for branch switching logic
fd -e ts -x grep -l "switchBranch\|selectBranch\|setActiveBranch" {} \; | head -10

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

# Get the full activeBranchData getter implementation
sed -n '80,88p' apps/web/client/src/components/store/editor/branch/manager.ts

Repository: onlook-dev/onlook

Length of output: 523


🏁 Script executed:

# Check if there are any async operations during branch switch that could cause divergence
rg -nP --type=ts 'switchToBranch|async.*branch' -A 10 apps/web/client/src/components/store/editor/branch/manager.ts | head -80

Repository: onlook-dev/onlook

Length of output: 2885


🏁 Script executed:

# Check if there's any scenario where currentBranchId could change between activeSandbox and activeBranch calls
rg -nP --type=ts 'branchMap' apps/web/client/src/components/store/editor/branch/manager.ts | head -30

Repository: onlook-dev/onlook

Length of output: 1336


🏁 Script executed:

# Look at the full context of the file being reviewed to understand the sequence
sed -n '75,115p' apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/context-pills/input-context-pills.tsx

Repository: onlook-dev/onlook

Length of output: 1915


🏁 Script executed:

# Verify if there are any safeguards preventing branch switches during file operations
rg -nP --type=ts 'fileExists|writeFile' -B 5 -A 5 apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/context-pills/input-context-pills.tsx | head -60

Repository: onlook-dev/onlook

Length of output: 1183


🏁 Script executed:

# Check if there's any locking mechanism or state guard in BranchManager
rg -nP --type=ts 'isSwitching|isLoading|locked' apps/web/client/src/components/store/editor/branch/manager.ts

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

# Search for patterns that guard against branch switches during operations
rg -nP --type=ts 'try.*finally|abort|cancel' apps/web/client/src/components/store/editor/ | grep -i branch | head -20

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

# Look for similar patterns in the codebase where branchId is captured during async ops
rg -nP --type=tsx 'activeBranch\.id.*await|await.*activeBranch\.id' -B 3 -A 3 | head -60

Repository: onlook-dev/onlook

Length of output: 89


🏁 Script executed:

# Search for how other code captures branchId before async operations
rg -nP --type=ts 'const.*branchId.*=.*branches\.activeBranch\.id' -A 5 | head -60

Repository: onlook-dev/onlook

Length of output: 785


🏁 Script executed:

# Look at the specific pattern: is branchId captured before or after async operations?
sed -n '75,115p' apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/context-pills/input-context-pills.tsx

Repository: onlook-dev/onlook

Length of output: 1915


🏁 Script executed:

# Check how the image-tab pattern handles this - does it capture branchId early?
sed -n '20,80p' apps/web/client/src/app/project/[id]/_components/left-panel/design-panel/image-tab/index.tsx

Repository: onlook-dev/onlook

Length of output: 1996


🏁 Script executed:

# Search for the pattern: capture branchId early before async operations
rg -nP --type=ts 'const branchId.*activeBranch.*id' -A 15 | head -80

Repository: onlook-dev/onlook

Length of output: 2055


🏁 Script executed:

# Confirm the exact issue: capture branchId after async ops vs before
# Show the full context around line 110 in the reviewed file
sed -n '103,112p' apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/context-pills/input-context-pills.tsx

Repository: onlook-dev/onlook

Length of output: 505


🏁 Script executed:

# Check if there are other similar patterns in the codebase that capture branchId late
rg -nP --type=ts 'await.*sandbox.*\n.*branchId.*activeBranch' | head -20

Repository: onlook-dev/onlook

Length of output: 43


Capture branchId before async operations to prevent mismatch.

The code writes files through editorEngine.activeSandbox, then captures branchId after multiple awaits (fileExists, fetch, writeFile). If the user switches branches during this sequence, the saved path will exist in one branch's sandbox but be tagged with a different branch's ID. Capture branchId early before file operations, similar to the pattern in image-tab/index.tsx line 22.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/client/src/app/project/`[id]/_components/right-panel/chat-tab/context-pills/input-context-pills.tsx
around lines 89 - 110, Capture the current branch id from
editorEngine.branches.activeBranch.id before any awaits and async file
operations (before the while loop/fetch/writeFile) and store it in a local const
(e.g., savedBranchId), then use that savedBranchId when building
updatedImageContext.branchId (instead of reading
editorEngine.branches.activeBranch.id after awaits); ensure the same
savedBranchId is used for any subsequent metadata related to
targetPath/imageContext to avoid branch mismatch.

displayName: savedImageName,
};

editorEngine.chat.context.context = editorEngine.chat.context.context.map((context) =>
context === imageContext ? updatedImageContext : context,
);

toast.success(`Saved image to ${targetPath}`);
} catch (error) {
console.error('Failed to save image to project assets:', error);
toast.error('Failed to save image to project assets');
}
Comment on lines +78 to +122
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded user-facing toast strings.

The toast messages on lines 78, 118 and 121 are hardcoded English strings shown to users. Route them through next-intl so they can be localized.

As per coding guidelines: "Avoid hardcoded user-facing text; use next-intl messages/hooks instead" (applies to apps/web/client/src/**/*.tsx).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/client/src/app/project/`[id]/_components/right-panel/chat-tab/context-pills/input-context-pills.tsx
around lines 78 - 122, Replace hardcoded toast strings in this component by
using next-intl translations: import and call useTranslations (e.g., const t =
useTranslations('ContextPills') at top of the component) and replace
toast.success('Image already exists in project assets') with
toast.success(t('image.alreadyExists')), replace toast.success(`Saved image to
${targetPath}`) with toast.success(t('image.saved', { path: targetPath })), and
replace toast.error('Failed to save image to project assets') with
toast.error(t('image.saveFailed')). Add the three keys (image.alreadyExists,
image.saved, image.saveFailed) to the appropriate locale message files and
update interpolation for path where needed; keep all other logic
(getSanitizedFileName, editorEngine.activeSandbox.writeFile,
editorEngine.chat.context mapping) unchanged.

};

const sortedContexts = useMemo(() => {
return [...editorEngine.chat.context.context]
.sort((a, b) => {
Expand All @@ -64,6 +140,7 @@ export const InputContextPills = observer(() => {
<ImagePill
key={key}
context={context as ImageMessageContext}
onSave={() => void handleSaveImageContext(context as ImageMessageContext)}
onRemove={() => handleRemoveContext(context)}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ export const useRepositoryImport = () => {
project: {
name: selectedRepo.name ?? 'New project',
description: selectedRepo.description || 'Imported from GitHub',
tags: ['github-import'],
},
userId: user.id,
sandboxId,
sandboxUrl: previewUrl,
git: {
repoUrl: selectedRepo.clone_url,
baseBranch: selectedRepo.default_branch,
},
});

if (!project) {
Expand Down
114 changes: 70 additions & 44 deletions apps/web/client/src/app/projects/import/page.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,94 @@
'use client';

import { useGetBackground } from '@/hooks/use-get-background';
import { Routes } from '@/utils/constants';
import { Card, CardDescription, CardHeader, CardTitle } from '@onlook/ui/card';
import { Icons } from '@onlook/ui/icons';
import { useRouter } from 'next/navigation';
import { TopBar } from '../_components/top-bar';

type ImportType = 'local' | 'github' | 'figma';

const importCards: {
type: ImportType;
title: string;
description: string;
icon: React.ReactNode;
ariaLabel: string;
}[] = [
{
type: 'local',
title: 'Import a Local Project',
description:
'Select a directory from your computer to start working with your project in Pixelraft.',
icon: <Icons.Upload className="w-6 h-6 text-primary" />,
ariaLabel: 'Import local project',
},
{
type: 'github',
title: 'Import from GitHub',
description:
'Connect your GitHub account to import repositories and create pull requests from Pixelraft.',
icon: <Icons.GitHubLogo className="w-6 h-6 text-primary" />,
ariaLabel: 'Import from GitHub',
},
{
type: 'figma',
title: 'Import from Figma',
description:
'Paste a Figma file URL to generate a Pixelraft-ready starter project and synced design metadata.',
icon: <Icons.Layout className="w-6 h-6 text-primary" />,
ariaLabel: 'Import from Figma',
},
];
Comment on lines +19 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

User-visible copy still says "Pixelraft" — contradicts the rebrand goal added in this same PR.

This same PR adds unified-onlook-implementation-plan.mdx whose M1 exit criteria require "no user-visible Pixelraft branding on active surfaces" and whose final acceptance criteria states "Pixelraft naming removed from user-visible surfaces." The three card descriptions on lines 23, 31, and 39 still reference "Pixelraft" / "Pixelraft-ready", which directly violates that intent on the Import page (a primary user-facing surface).

✏️ Proposed copy fix
-        description:
-            'Select a directory from your computer to start working with your project in Pixelraft.',
+        description:
+            'Select a directory from your computer to start working with your project in Onlook.',
@@
-        description:
-            'Connect your GitHub account to import repositories and create pull requests from Pixelraft.',
+        description:
+            'Connect your GitHub account to import repositories and create pull requests from Onlook.',
@@
-        description:
-            'Paste a Figma file URL to generate a Pixelraft-ready starter project and synced design metadata.',
+        description:
+            'Paste a Figma file URL to generate an Onlook-ready starter project and synced design metadata.',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/app/projects/import/page.tsx` around lines 19 - 43, The
three user-facing card descriptions still reference "Pixelraft"; update the
description strings in the objects with type 'local', 'github', and 'figma' to
remove the "Pixelraft" branding (and "Pixelraft-ready") and replace with the new
non-branded phrasing (e.g., "the app", "your project", or the new product name),
leaving icons/ariaLabel values unchanged; locate the objects by their type
property in the cards array in page.tsx and edit only the description text.


const importPathMap: Record<ImportType, string> = {
local: `${Routes.IMPORT_PROJECT}/local`,
github: Routes.IMPORT_GITHUB,
figma: Routes.IMPORT_FIGMA,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Disable Figma import card until route exists

This maps the new Figma card to Routes.IMPORT_FIGMA (/projects/import/figma), but there is no corresponding App Router page under apps/web/client/src/app/projects/import/figma, so clicking a primary import option now sends users to a 404. Either add that route or keep the card non-clickable/hidden until the flow is implemented.

Useful? React with 👍 / 👎.

};

const Page = () => {
const router = useRouter();
const handleCardClick = (type: 'local' | 'github') => {
router.push(`/projects/import/${type}`);
};
const backgroundUrl = useGetBackground('create');

const handleCardClick = (type: ImportType) => {
router.push(importPathMap[type]);
};

return (
<div className="w-screen h-screen flex flex-col"
<div
className="w-screen h-screen flex flex-col"
style={{
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundImage: `url(${backgroundUrl})`,
}}
>
<TopBar />
<div className="flex items-center justify-center overflow-hidden max-w-4xl mx-auto w-full flex-1 gap-6 p-6 select-none">
<Card
className={`w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50`}
onClick={() => handleCardClick('local')}
tabIndex={0}
role="button"
aria-label="Import local project"
>
<CardHeader className="flex flex-col justify-between h-full">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center select-none">
<Icons.Upload className="w-6 h-6 text-primary" />
</div>
<div className="space-y-2">
<CardTitle className="text-title3">Import a Local Project</CardTitle>
<CardDescription className="text-sm text-balance">
Select a directory from your computer to start working with your project in Onlook.
</CardDescription>
</div>
</CardHeader>
</Card>
{/* Temporary disabled */}
<Card
className={'w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50 cursor-not-allowed opacity-60'}
onClick={() => false && handleCardClick('github')}
tabIndex={0}
role="button"
aria-label="Connect to GitHub"
>
<CardHeader className="flex flex-col justify-between h-full">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center select-none">
<Icons.GitHubLogo className="w-6 h-6 text-primary" />
</div>
<div className="space-y-2">
<CardTitle className="text-title3">Import from GitHub</CardTitle>
<CardDescription className="text-sm text-balance">
Connect your GitHub account to access and work with your repositories
</CardDescription>
</div>
</CardHeader>
</Card>
<div className="flex items-center justify-center overflow-hidden max-w-6xl mx-auto w-full flex-1 gap-6 p-6 select-none">
{importCards.map((card) => (
<Card
key={card.type}
className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50"
onClick={() => handleCardClick(card.type)}
tabIndex={0}
role="button"
aria-label={card.ariaLabel}
>
Comment on lines +71 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Card exposed as role="button" is not keyboard-activatable.

The <Card> renders as a div with onClick, tabIndex={0}, and role="button", but there is no onKeyDown/onKeyUp handler. Keyboard users who focus the card cannot trigger navigation with Enter or Space, which breaks the implied button contract and the keyboard-nav requirements outlined in the same PR's plan (DQA-04/CMP-05). Either render an actual <button> (or <a> for navigation) or attach a key handler.

♿ Proposed accessibility fix
-                    <Card
-                        key={card.type}
-                        className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50"
-                        onClick={() => handleCardClick(card.type)}
-                        tabIndex={0}
-                        role="button"
-                        aria-label={card.ariaLabel}
-                    >
+                    <Card
+                        key={card.type}
+                        className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
+                        onClick={() => handleCardClick(card.type)}
+                        onKeyDown={(e) => {
+                            if (e.key === 'Enter' || e.key === ' ') {
+                                e.preventDefault();
+                                handleCardClick(card.type);
+                            }
+                        }}
+                        tabIndex={0}
+                        role="button"
+                        aria-label={card.ariaLabel}
+                    >

A focus-visible style is also worth adding so the focused card is discoverable. Based on learnings, this surface should ideally use a real <button> element where possible.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Card
key={card.type}
className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50"
onClick={() => handleCardClick(card.type)}
tabIndex={0}
role="button"
aria-label={card.ariaLabel}
>
<Card
key={card.type}
className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
onClick={() => handleCardClick(card.type)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleCardClick(card.type);
}
}}
tabIndex={0}
role="button"
aria-label={card.ariaLabel}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/app/projects/import/page.tsx` around lines 71 - 78, The
Card rendered with role="button" (component Card) and onClick handler is not
keyboard-activatable; update the component usage so keyboard users can trigger
handleCardClick(card.type): either replace the Card wrapper with a semantic
interactive element (<button> or <a>) so it natively supports Enter/Space and
focus styles, or add an onKeyDown handler that listens for Enter and Space and
calls handleCardClick(card.type) and preventDefault for Space; also ensure
tabIndex remains 0 and add a visible focus style (focus-visible) so focused
cards are discoverable.

<CardHeader className="flex flex-col justify-between h-full">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center select-none">
{card.icon}
</div>
<div className="space-y-2">
<CardTitle className="text-title3">{card.title}</CardTitle>
<CardDescription className="text-sm text-balance">
{card.description}
</CardDescription>
</div>
</CardHeader>
</Card>
))}
</div>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/web/client/src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
userCanvasRouter,
userRouter,
utilsRouter,
workspaceRouter,
} from './routers';
import { branchRouter } from './routers/project/branch';

Expand All @@ -37,6 +38,7 @@ export const appRouter = createTRPCRouter({
member: memberRouter,
domain: domainRouter,
github: githubRouter,
workspace: workspaceRouter,
subscription: subscriptionRouter,
usage: usageRouter,
publish: publishRouter,
Expand Down
Loading