Skip to content

Conversation

@drfarrell
Copy link
Collaborator

@drfarrell drfarrell commented Aug 23, 2025

Description

  • Brand panel styling to pull-in colors (and a pre-loading state)
  • pre-loading for the credits dropdown
  • style improvements to the window controls (which btw the theme selector doesn't work)
  • slight adjustments to the home page for smaller screens

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Release
  • Refactor
  • Other (please describe):

Important

Styling improvements include brand panel color loading, hiring promo visibility adjustments, and UI component refinements for better user experience.

  • Behavior:
    • Hiring promo hides on short screens in index.tsx.
    • Brand colors load from theme and auto-refresh on config changes in color-panel/index.tsx.
    • Plans panel shows loading skeletons while data loads in plans.tsx.
  • UI Components:
    • Consistent icon sizing and selection highlighting in device-selector.tsx.
    • Flex controls (alignment/direction) appear only when flex layout is active in align.tsx, direction.tsx.
    • Gap input expands to fill space in gap.tsx.
  • Style:
    • Spacing/width refinements across toolbar buttons and header/hero padding in create.tsx, index.tsx.
    • Color panel header shows back arrow and inline title in color-panel/index.tsx.

This description was created by Ellipsis for b807ae3. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Hiring promo now hides on short screens.
  • Improvements

    • Brand colors load from theme and auto-refresh on config changes; shows skeletons while loading.
    • Device selector: consistent icon sizing, selection highlighting, refined labels, contextual tooltip (disabled when open).
    • Flex controls (alignment/direction) only appear when flex layout is active; gap input expands to fill space.
    • Overflow/menu and device tooltips added; rotate and theme controls unified for consistent sizing.
  • Style

    • Numerous spacing, padding, and width refinements across toolbar buttons, hero, header, and panels.

@drfarrell drfarrell requested a review from Kitenite August 23, 2025 00:21
@vercel
Copy link

vercel bot commented Aug 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
web Ready Ready Preview Comment Aug 25, 2025 8:59pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
docs Skipped Skipped Aug 25, 2025 8:59pm

@supabase
Copy link

supabase bot commented Aug 23, 2025

This pull request has been ignored for the connected project wowaemfasoptxrdjhilu because there are no changes detected in apps/backend/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

Adds viewport-based gating for the hero promo, widespread UI/className and spacing tweaks across toolbar and mockups, introduces flex-only guards for display controls, refactors device selector/tooltips and button sizing, loads brand colors from theme with a file watcher, and adds loading skeletons for subscription UI.

Changes

Cohort / File(s) Summary
Hero responsiveness
apps/web/client/src/app/_components/hero/index.tsx, apps/web/client/src/app/_components/hero/create.tsx
Adds isShortScreen state + resize effect to hide promo on short viewports; adjusts top padding and minor card header/content className tweaks (styling only).
Display controls (flex gating & layout)
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/index.tsx, .../type.tsx, .../align.tsx, .../direction.tsx, .../gap.tsx
Adds guards so Align/Direction/Gaps render only when computed display is flex/inline-flex; adjusts label widths, gaps, container spacing; adds HorizontalAlignInput and GapInput; tweaks dropdown sizing/padding (styling/layout only).
Toolbar & trigger sizing
apps/web/client/src/app/project/[id]/_components/editor-bar/index.tsx, .../dropdowns/{border,border-color,margin,padding,radius}.tsx, .../dropdowns/opacity.tsx, .../inputs/input-image.tsx, .../text-inputs/advanced-typography.tsx, .../text-inputs/text-align.tsx, .../text-inputs/font/font-size.tsx, apps/web/client/src/app/_components/shared/mockups/tailwind-color-editor.tsx, .../overflow-menu.tsx
Many trigger buttons reduced from w-10/min-w-10w-9/min-w-9; small padding/spacing adjustments; opacity trigger removed fixed h-8; overflow trigger wrapped with HoverOnlyTooltip; display dropdown min-width and inner padding adjusted.
Device selector & window actions
apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/device-selector.tsx, .../rotate-group.tsx, .../theme-group.tsx, .../window-actions-group.tsx
Device icons accept className (centralized sizing/styling); selection visual states refined; tooltip shown only when closed; rotate button replaced with local ToolbarButton (w-9); theme & window-action buttons sizing and active visuals adjusted.
Brand tab dynamic colors
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx, apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/color-panel/index.tsx
Loads brand colors from theme config (preload + fileEventBus watcher for tailwind/globals changes); UI switched to 12-column grid with skeleton placeholders; header changed to back-arrow + title row (logic addition: async load + watcher).
Plans loading states
apps/web/client/src/components/ui/avatar-dropdown/plans.tsx
Adds isLoading flags for subscription & usage queries; shows skeleton placeholders while loading; guards usagePercent calculation; preserves debounced refetch-on-open.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant W as Window
  participant Hero as HeroComponent
  Note over Hero: viewport height gating
  U->>W: load / resize
  W->>Hero: read window.innerHeight
  Hero->>Hero: set isShortScreen = h < 700
  alt isShortScreen
    Hero-->>U: render header without promo
  else
    Hero-->>U: render header with promo
  end
Loading
sequenceDiagram
  autonumber
  participant User
  participant TypeInput as DisplayTypeInput
  participant Style as editorEngine.style
  Note over TypeInput: selecting display type
  User->>TypeInput: select value
  alt value == "flex"
    TypeInput->>Style: updateMultiple({display:flex,flex-direction:row,align-items:flex-start,justify-content:flex-start})
  else
    TypeInput->>Style: update(display:value)
    TypeInput->>Style: updateMultiple({flex-direction:initial,align-items:initial,justify-content:initial})
  end
  Note over Align/Dir: Align/Direction inputs render only when display is flex/inline-flex
Loading
sequenceDiagram
  autonumber
  participant BT as BrandTab
  participant Theme as ThemeConfig
  participant Bus as fileEventBus
  Note over BT: load & sync brand colors
  BT->>Theme: scan colorGroups/defaults
  Theme-->>BT: brandColors
  BT->>BT: setState(brandColors)
  Bus->>BT: file change (tailwind config or globals.css)
  BT->>Theme: re-scan
  Theme-->>BT: updated brandColors
  BT->>BT: setState(updated)
Loading
sequenceDiagram
  autonumber
  participant DS as DeviceSelector
  participant UI as Tooltip
  participant User
  User->>DS: open dropdown
  DS->>DS: set isOpen = true
  DS-->>UI: disable tooltip
  User->>DS: close dropdown
  DS->>DS: set isOpen = false
  DS-->>UI: enable tooltip("Device")
Loading
sequenceDiagram
  autonumber
  participant Plans as PlansPanel
  participant API as BillingAPI
  participant UI as Skeletons
  Plans->>API: fetch subscription & usage
  Plans-->>UI: show skeletons
  API-->>Plans: return data
  Plans-->>UI: render product/price/usage, hide skeletons
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I’m a rabbit who tidies the UI tree,
Slim buttons hop in a neat row for me.
Flex stays hidden till its flag is true,
Colors bloom when tailwind says renew.
Tooltips wink, skeletons hush—happy deploy, whee! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bccebcb and b807ae3.

📒 Files selected for processing (1)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/device-selector.tsx (3 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch styling-for-demo

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/client/src/app/project/[id]/_components/editor-bar/inputs/input-image.tsx (1)

104-109: Dropdown can’t close via outside click (onOpenChange ignores false)

handleOpenChange only forwards true, so outside clicks or ESC won’t close the menu. Forward the actual state.

-    const handleOpenChange = (open: boolean) => {
-        if (open) {
-            onOpenChange(true);
-        }
-    };
+    const handleOpenChange = (open: boolean) => {
+        onOpenChange(open);
+    };

After this, confirm the dropdown closes on outside click and ESC.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/direction.tsx (1)

37-43: Don’t clobber inline-flex on change; preserve the current display mode.

When the user is currently on inline-flex, changing direction forces display to flex, altering layout. Preserve the existing mode.

Apply:

 onChange={(newValue) => {
   setValue(newValue);
-  editorEngine.style.updateMultiple({
-      'flex-direction': newValue,
-      display: 'flex',
-  });
+  const currentDisplay = editorEngine.style.selectedStyle?.styles.computed.display;
+  editorEngine.style.updateMultiple({
+    'flex-direction': newValue,
+    display: currentDisplay === 'inline-flex' ? 'inline-flex' : 'flex',
+  });
 }}

If the display is already flex/inline-flex, you could also omit setting display entirely; the above is the least disruptive change.

🧹 Nitpick comments (28)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/color-panel/index.tsx (1)

80-90: Back button needs an accessible name; consider sticky header to avoid stacking issues

  • The icon-only button has no accessible name. Add an aria-label/title or sr-only text so screen readers announce its purpose.
  • Optional: switching from fixed to sticky reduces z-index/overlay risks in nested scroll containers.

Apply this diff to add accessibility and (optionally) switch to sticky:

-            <div className="border-border bg-background fixed top-0 right-0 left-0 z-10 flex items-center justify-start border-b py-1.5 pr-2.5 pl-3 gap-2">
+            <div className="border-border bg-background sticky top-0 z-10 flex items-center justify-start border-b py-1.5 pr-2.5 pl-3 gap-2">
                 <Button
                     variant="ghost"
                     size="icon"
                     className="hover:bg-background-secondary h-7 w-7 rounded-md"
                     onClick={handleClose}
+                    aria-label="Back to Brand Tab"
+                    title="Back"
                 >
-                    <Icons.ArrowLeft className="h-4 w-4" />
+                    <Icons.ArrowLeft className="h-4 w-4" aria-hidden />
                 </Button>
                 <h2 className="text-foreground text-sm font-normal">Brand Colors</h2>
             </div>
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (5)

16-21: Make color tiles non-interactive for screen readers and avoid misleading pointer cursor

The square itself isn’t clickable (parent handles onClick). Mark it decorative and avoid pointer on the child to reduce confusion.

-const ColorSquare = ({ color }: ColorSquareProps) => (
-    <div
-        className="w-full aspect-square cursor-pointer"
-        style={{ backgroundColor: color }}
-    />
-);
+const ColorSquare = ({ color }: ColorSquareProps) => (
+    <div
+        className="w-full aspect-square"
+        aria-hidden
+        role="presentation"
+        style={{ backgroundColor: color }}
+    />
+);

27-33: Split concerns and avoid re-running theme scan due to broad dependencies

This effect mixes font and theme preloading and depends on editorEngine.theme, which can retrigger scans unnecessarily.

  • Keep the font scan keyed to fontConfigPath.
  • Run theme.scanConfig() once on mount (or let the colors effect handle it).
  • Verify that editorEngine.theme object identity is stable; otherwise this effect may rerun more than intended.

Proposed change within this effect:

-    // Pre-load theme configuration to avoid delay when opening color panel
-    editorEngine.theme.scanConfig();
-}, [editorEngine.font.fontConfigPath, editorEngine.theme]);
+}, [editorEngine.font.fontConfigPath]);

And add a separate mount-only effect (outside this range):

useEffect(() => {
    editorEngine.theme.scanConfig();
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

35-69: Deduplicate color extraction and avoid redundant scan; cap to visible swatch count

  • You call scanConfig() here after already preloading. If preloading remains, drop the extra scan to reduce I/O.
  • The extraction logic appears twice in the file; centralize it to a helper to keep behavior in one place.
  • The grid renders only a single 12-column row; slice to 12 here to match the UI and avoid unnecessary DOM work.
  • Deduplicate colors to avoid repeated swatches when 500/default collide across groups/defaults.

Apply these focused edits:

-            await editorEngine.theme.scanConfig();
             const { colorGroups, colorDefaults } = editorEngine.theme;
@@
-            setBrandColors(projectColors);
+            // unique + cap to the 12 tiles we actually render
+            setBrandColors(Array.from(new Set(projectColors)).slice(0, 12));

And add a small helper (outside this range) to reuse in both effects:

function extractProjectColors(theme: any): string[] {
    const out: string[] = [];
    const { colorGroups, colorDefaults } = theme;

    const pick = (group: any[]) => {
        group.forEach((color) => {
            if (color?.name === '500' || color?.name?.toLowerCase?.() === 'default') {
                out.push(color.lightColor);
            }
        });
    };

    Object.values(colorGroups).forEach(pick);
    Object.values(colorDefaults).forEach(pick);
    return Array.from(new Set(out)); // unique
}

71-112: Debounce file-change handling and clear pending timers on unmount

  • Using setTimeout without clearing on unmount can set state after unmount.
  • Rapid successive change events (common on save) will enqueue multiple scans.

Minimal changes to introduce a debounced refresh:

  1. Import useRef:
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
  1. Add a ref above the effect:
const refreshTimerRef = useRef<number | null>(null);
  1. Replace the inner setTimeout block:
-                // Rescan for new color groups
-                setTimeout(async () => {
+                // Debounced rescan for new color groups
+                if (refreshTimerRef.current) {
+                    window.clearTimeout(refreshTimerRef.current);
+                }
+                refreshTimerRef.current = window.setTimeout(async () => {
                     try {
-                        await editorEngine.theme.scanConfig();
-                        
-                        const { colorGroups, colorDefaults } = editorEngine.theme;
-                        
-                        // Extract color-500 variants from project colors
-                        const projectColors: string[] = [];
-                        
-                        // Add colors from custom color groups (user-defined in Tailwind config)
-                        Object.values(colorGroups).forEach(group => {
-                            group.forEach(color => {
-                                // Get the default/500 color from each custom color group
-                                if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
-                                    projectColors.push(color.lightColor);
-                                }
-                            });
-                        });
-                        
-                        // Add colors from default color groups (standard Tailwind colors)
-                        Object.values(colorDefaults).forEach(group => {
-                            group.forEach(color => {
-                                // Get the default/500 color from each default color group
-                                if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
-                                    projectColors.push(color.lightColor);
-                                }
-                            });
-                        });
-                        
-                        setBrandColors(projectColors);
+                        await editorEngine.theme.scanConfig();
+                        setBrandColors(extractProjectColors(editorEngine.theme).slice(0, 12));
                     } catch (error) {
                         console.warn('Theme scanning failed:', error);
                     }
-                }, 100); // Small delay to ensure file is fully written
+                }, 150); // small debounce to ensure file is fully written
  1. Clear any pending timers in cleanup:
         return () => {
             unsubscribe();
+            if (refreshTimerRef.current) {
+                window.clearTimeout(refreshTimerRef.current);
+            }
         };

Also verify that extractProjectColors (proposed earlier) is in scope.

Finally, confirm that animate-shimmer is defined in your Tailwind config; otherwise skeletons won’t animate.


149-165: Align rendering to a single-row summary tile and improve keyboard/a11y behavior

  • The container is fixed to 40px height; rendering more than 12 colors creates hidden extra rows. Slice to 12.
  • Make the clickable grid keyboard-accessible with role/tabIndex and an onKeyDown handler.
  • Mark skeleton cells as aria-hidden.
-                    <div 
-                        className="grid grid-cols-12 gap-0 rounded-lg overflow-hidden h-[40px] max-h-[40px] bg-background-onlook border-[0.5px] border-white/50 hover:border-[0.5px] hover:border-white cursor-pointer hover:border-transparent transition-all duration-200"
-                        onClick={() => (editorEngine.state.brandTab = BrandTabValue.COLORS)}
-                    >
-                        {brandColors.length > 0 ? (
-                            brandColors.map((color, index) => (
+                    <div
+                        className="grid grid-cols-12 gap-0 rounded-lg overflow-hidden h-[40px] max-h-[40px] bg-background-onlook border-[0.5px] border-white/50 hover:border-[0.5px] hover:border-white cursor-pointer hover:border-transparent transition-all duration-200"
+                        role="button"
+                        tabIndex={0}
+                        aria-label="Open Brand Colors"
+                        onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && (editorEngine.state.brandTab = BrandTabValue.COLORS)}
+                        onClick={() => (editorEngine.state.brandTab = BrandTabValue.COLORS)}
+                    >
+                        {brandColors.length > 0 ? (
+                            brandColors.slice(0, 12).map((color, index) => (
                                 <ColorSquare key={`brand-color-${index}`} color={color} />
                             ))
                         ) : (
                             Array.from({ length: 12 }, (_, index) => (
                                 <div
                                     key={`loading-color-${index}`}
-                                    className="w-full h-full bg-gradient-to-r from-gray-300 via-gray-400 to-gray-300 bg-[length:200%_100%] animate-shimmer"
+                                    className="w-full h-full bg-gradient-to-r from-gray-300 via-gray-400 to-gray-300 bg-[length:200%_100%] animate-shimmer"
+                                    aria-hidden
                                 />
                             ))
                         )}
                     </div>
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/padding.tsx (2)

126-133: Align tooltip behavior with other triggers to avoid showing while open

Other triggers disable HoverOnlyTooltip when the dropdown is open and hide the arrow. Mirror that here for consistency and to prevent tooltip flicker.

Apply:

-            <HoverOnlyTooltip content="Padding" side="bottom">
+            <HoverOnlyTooltip content="Padding" side="bottom" className="mt-1" hideArrow disabled={isOpen}>

129-133: Consistent width token across toolbar

Most triggers use min-w-9; this matches the pattern and looks fine. If you end up standardizing, consider a ToolbarButton size prop (e.g., size="sm") to encode this width centrally.

apps/web/client/src/app/project/[id]/_components/editor-bar/inputs/input-image.tsx (1)

144-147: Prefer min-w-9 over fixed w-9 for consistency and layout resilience

Other triggers use min-w-9. Using min width avoids potential jitter if the spinner or icon variants slightly vary. Not critical, but keeps things uniform.

-                            className="flex w-9 flex-col items-center justify-center gap-0.5 relative"
+                            className="flex min-w-9 flex-col items-center justify-center gap-0.5 relative"
apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/font/font-size.tsx (1)

103-112: Add basic a11y and input constraints to the number field.

Small wins that don’t change behavior but improve UX/a11y:

  • Provide an accessible name so SR users hear “Font size”.
  • Hint numeric keypad on mobile via inputMode.
  • Add min/step to match your runtime validation.

You can apply this outside the changed hunk:

<input
  ref={inputRef}
  type="number"
  min={1}
  step={1}
  inputMode="numeric"
  aria-label="Font size"
  value={inputValue}
  onChange={handleInputChange}
  onKeyDown={handleInputKeyDown}
  onBlur={handleInputBlur}
  onClick={(e) => e.stopPropagation()}
  className="w-full bg-transparent text-center text-sm focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>

Optionally, consider adding tabular-nums to the className to avoid digit-width jitter.

apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/window-actions-group.tsx (1)

43-53: Shrinking to w-9 reduces hit area; add aria-labels and verify target-size.

  • w-9 (≈36px) is under the 44px tap target recommended by Apple/Material. It still satisfies WCAG 2.2 AA (24px), but please verify comfort on touch devices.
  • Buttons rely on icons only; add aria-labels for SR users.

Two minimal changes that keep the new width:

 <ToolbarButton
-  className="flex items-center w-9"
+  className="flex items-center w-9"
+  aria-label="Duplicate window"
   onClick={duplicateWindow}
   disabled={isDuplicating}
 >
...
 <ToolbarButton
-  className="flex items-center w-9"
+  className="flex items-center w-9"
+  aria-label="Delete window"
   disabled={!editorEngine.frames.canDelete() || isDeleting}
   onClick={deleteWindow}
 >

If you want to preserve the slimmer visuals but meet a 44px hit target, consider adding a larger invisible hitbox via utility classes (e.g., relative after:absolute after:-inset-1 after:content-['']) on the ToolbarButton.

Also applies to: 57-69

apps/web/client/src/app/_components/hero/create.tsx (1)

259-265: Spacing changes LGTM; make the Card width responsive to narrow screens.

Adding p-4 and tighter gaps reads better. Since this component is referenced in home/hero contexts, guard the fixed 600px width to avoid overflow on small devices.

Apply:

- 'w-[600px] overflow-hidden gap-1.5 backdrop-blur-md bg-background/20 p-4',
+ 'w-[600px] max-w-full overflow-hidden gap-1.5 backdrop-blur-md bg-background/20 p-4',

Alternatively: 'w-full sm:w-[600px]' if you want it to expand fluidly below sm.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/direction.tsx (1)

22-29: Good guard: only render when flex is active.

Conditionalizing on computed display prevents irrelevant controls from showing. Minor simplification:

-const displayValue = editorEngine.style.selectedStyle?.styles.computed.display;
-const isFlexboxActive = displayValue === 'flex' || displayValue === 'inline-flex';
+const displayValue = editorEngine.style.selectedStyle?.styles.computed.display;
+const isFlexboxActive = (displayValue ?? '').includes('flex');

This also future-proofs for any other flex-like tokens.

apps/web/client/src/app/_components/shared/mockups/tailwind-color-editor.tsx (1)

211-223: Mock toolbar width trims are fine; mirror a11y where practical.

Reducing to min-w-9 is consistent with the editor bar updates. Since this mock mirrors production styling, consider also adding aria-labels to icon-only buttons (e.g., “Align left”, “Advanced typography”, “More options”) to keep examples aligned with best practices.

Example:

<ToolbarButton className="flex items-center justify-center min-w-9" aria-label="Align left">
  {React.createElement(Icons.TextAlignLeft as any, { className: 'h-4 w-4' } as any)}
</ToolbarButton>
apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/theme-group.tsx (2)

37-43: Fix hover text-color override on active theme buttons and add accessible names.

ToolbarButton’s base classes include hover:text-white, which will override your active text-foreground-primary on hover. Add hover:text-foreground-primary for the active branch to avoid the flash to white. Also, these are icon-only buttons; tooltips don’t provide an accessible name. Add aria-labels.

-                    <ToolbarButton
-                        className={`w-9 ${theme === SystemTheme.SYSTEM ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary' : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`}
+                    <ToolbarButton
+                        aria-label="System theme"
+                        className={`w-9 ${theme === SystemTheme.SYSTEM ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary hover:text-foreground-primary' : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`}
                         onClick={() => changeTheme(SystemTheme.SYSTEM)}
                     >
...
-                    <ToolbarButton
-                        className={`w-9 ${theme === SystemTheme.DARK ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary' : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`}
+                    <ToolbarButton
+                        aria-label="Dark theme"
+                        className={`w-9 ${theme === SystemTheme.DARK ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary hover:text-foreground-primary' : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`}
                         onClick={() => changeTheme(SystemTheme.DARK)}
                     >
...
-                    <ToolbarButton
-                        className={`w-9 ${theme === SystemTheme.LIGHT ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary' : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`}
+                    <ToolbarButton
+                        aria-label="Light theme"
+                        className={`w-9 ${theme === SystemTheme.LIGHT ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary hover:text-foreground-primary' : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`}
                         onClick={() => changeTheme(SystemTheme.LIGHT)}
                     >

Also applies to: 45-51, 53-59


37-59: Reduce duplication by extracting a helper for button classes.

The three className templates differ only by the active check. Consider extracting a small helper to improve maintainability.

Example outside the changed lines:

const themeBtnClass = (active: boolean) =>
  `w-9 ${active
    ? 'bg-background-tertiary/50 hover:bg-background-tertiary/50 text-foreground-primary hover:text-foreground-primary'
    : 'hover:bg-background-tertiary/50 text-foreground-onlook'}`;

Usage:

<ToolbarButton aria-label="System theme" className={themeBtnClass(theme === SystemTheme.SYSTEM)} ... />
apps/web/client/src/app/_components/hero/index.tsx (1)

18-30: Prevent initial flash by deriving isShortScreen in the state initializer.

On short screens, the promo renders once then disappears after the effect runs. Initialize from window.innerHeight to avoid the flicker.

-    const [isShortScreen, setIsShortScreen] = useState(false);
+    const [isShortScreen, setIsShortScreen] = useState(
+        () => typeof window !== 'undefined' && window.innerHeight < 700
+    );

Optional: throttle the resize handler (e.g., requestAnimationFrame or lodash/throttle) if resize performance becomes a concern.

apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/rotate-group.tsx (1)

10-19: Add an accessible name to the icon-only rotate button.

Improves screen reader support; tooltips don’t supply accessible names.

-            <ToolbarButton
+            <ToolbarButton
+                aria-label="Rotate device"
                 className="w-9"
                 onClick={() => {
                     const { width, height } = frameData.frame.dimension;
                     frameData.frame.dimension.width = height;
                     frameData.frame.dimension.height = width;
                 }}
             >
apps/web/client/src/components/ui/avatar-dropdown/plans.tsx (1)

31-31: Clamp usagePercent to 100 to avoid overflow.

When usageCount exceeds limitCount, the progress bar can exceed bounds.

-    const usagePercent = usage && usage.limitCount > 0 ? usage.usageCount / usage.limitCount * 100 : 0;
+    const usagePercent =
+        usage && usage.limitCount > 0
+            ? Math.min(100, (usage.usageCount / usage.limitCount) * 100)
+            : 0;
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/gap.tsx (1)

26-38: Avoid writing invalid CSS when the input is empty or non-numeric.

Currently, clearing the input could write values like "px" or "em". Guard updates to only commit valid numeric values.

                 <InputIcon
                     value={numValue}
                     unit={unitValue}
                     onChange={(newValue) => {
                         setNumValue(newValue);
-                        editorEngine.style.update('gap', `${newValue}${unitValue}`);
+                        if (newValue !== '' && !Number.isNaN(Number(newValue))) {
+                            editorEngine.style.update('gap', `${newValue}${unitValue}`);
+                        }
                     }}
                     onUnitChange={(newUnit) => {
                         setUnitValue(newUnit);
-                        editorEngine.style.update('gap', `${numValue}${newUnit}`);
+                        if (numValue !== '' && !Number.isNaN(Number(numValue))) {
+                            editorEngine.style.update('gap', `${numValue}${newUnit}`);
+                        }
                     }}
                 />

Optional: Clamp negative values to 0 since CSS gap cannot be negative.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/align.tsx (1)

72-75: Label width consistency verified; optional axis-aware labels
All Display dropdown labels (Direction, Gap, Vertical, Horizontal) uniformly use w-20, so there’s no width mismatch or panel jiggle.

Affected files/locations:

  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/direction.tsx (line 33)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/gap.tsx (line 24)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/align.tsx (lines 74, 109)

Optional refactor: switch static “Vertical”/“Horizontal” labels to reflect the current flexDirection. For example:

const isColumn = editorEngine.style.selectedStyle?.styles.computed.flexDirection === 'column';
<span className="text-sm text-muted-foreground w-20">
  {isColumn ? 'Horizontal' : 'Vertical'}
</span>
apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/device-selector.tsx (2)

11-24: DRY icon sizing; centralize the base class.

Both DeviceIcon and CustomIcon compute identical size classes. Define a single baseIconClass and reuse it to keep sizes consistent and simpler to tweak in the future.

Apply this diff:

+const baseIconClass = 'h-3.5 w-3.5 min-h-3.5 min-w-3.5';
-const DeviceIcon = ({ deviceType, orientation, className }: { deviceType: string, orientation: Orientation, className?: string }) => {
-    const iconClassName = `h-3.5 w-3.5 min-h-3.5 min-w-3.5 ${className || ''}`;
+const DeviceIcon = ({ deviceType, orientation, className }: { deviceType: string, orientation: Orientation, className?: string }) => {
+    const iconClassName = `${baseIconClass} ${className || ''}`;
@@
-        default:
-            return <CustomIcon orientation={orientation} className={className} />;
+        default:
+            return <CustomIcon orientation={orientation} className={className} />;
     }
 };
 
-const CustomIcon = ({ orientation, className }: { orientation: Orientation, className?: string }) => {
-    const iconClassName = `h-3.5 w-3.5 min-h-3.5 min-w-3.5 ${className || ''}`;
+const CustomIcon = ({ orientation, className }: { orientation: Orientation, className?: string }) => {
+    const iconClassName = `${baseIconClass} ${className || ''}`;

Note: Passing className to CustomIcon is fine because it recomputes with the same base size.

Also applies to: 27-34


110-118: Minor cleanup: reduce repeated selection checks in item rendering.

The selection condition is repeated three times (item class, icon color, micro-dimension color). Consider computing const isSelected = device === \${category}:${name}`` in the map callback and reusing it to simplify classNames and avoid drift.

If you prefer a minimal change without refactoring the map:

-                                className={`text-xs flex items-center cursor-pointer ${
-                                    device === `${category}:${name}` 
-                                        ? 'bg-background-tertiary/50 text-foreground-primary' 
-                                        : ''
-                                }`}
+                                className={`text-xs flex items-center cursor-pointer ${device === `${category}:${name}` ? 'bg-background-tertiary/50 text-foreground-primary' : ''}`}

Optional: adopt a cn() util to compose classes more readably across the file.

apps/web/client/src/app/project/[id]/_components/editor-bar/index.tsx (1)

100-100: Padding tweak LGTM; consider avoiding half-pixel borders for crisper rendering.

border-[0.5px] can render blurry on some displays/browsers. A 1px border with reduced opacity typically looks sharper while preserving the light-weight look.

Apply this diff:

-                className={cn(
-                    'flex flex-col border-[0.5px] border-border p-1 px-1 bg-background rounded-xl backdrop-blur drop-shadow-xl z-50 overflow-hidden',
+                className={cn(
+                    'flex flex-col border border-border/50 p-1 px-1 bg-background rounded-xl backdrop-blur drop-shadow-xl z-50 overflow-hidden',
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/opacity.tsx (1)

82-82: Stray Tailwind token “hover” in Input className.

hover without a utility is a no-op and can trigger unnecessary class churn in JIT. Suggest removing it.

-                            className="px-1 w-8 text-left data-[state=open]:text-white text-small focus:text-foreground-primary focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none !bg-transparent border-none group-hover:text-foreground-primary focus:ring-0 focus:outline-none text-muted-foreground !hide-spin-buttons no-focus-ring [appearance:textfield] group-hover:text-foreground-primary transition-colors duration-150 hover"
+                            className="px-1 w-8 text-left data-[state=open]:text-white text-small focus:text-foreground-primary focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none !bg-transparent border-none group-hover:text-foreground-primary focus:ring-0 focus:outline-none text-muted-foreground !hide-spin-buttons no-focus-ring [appearance:textfield] group-hover:text-foreground-primary transition-colors duration-150"
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/index.tsx (1)

58-59: Guard against small-view overflows in the dropdown.

Bumping min width to 250px improves content fit, but can overflow on narrow viewports. Add a responsive min width and an upper bound.

-            <DropdownMenuContent align="start" className="min-w-[250px] mt-2 p-1.5 rounded-lg">
+            <DropdownMenuContent align="start" className="min-w-[220px] sm:min-w-[250px] max-w-[90vw] mt-2 p-1.5 rounded-lg">
apps/web/client/src/app/project/[id]/_components/editor-bar/overflow-menu.tsx (1)

29-35: Add ARIA state to improve screen-reader context.

Expose popup semantics and open state on the trigger.

-                    <DropdownMenuTrigger asChild>
+                    <DropdownMenuTrigger asChild>
                         <ToolbarButton
                             isOpen={isOpen}
-                            className="w-9 flex items-center justify-center"
+                            className="w-9 flex items-center justify-center"
                             aria-label="Show more toolbar controls"
+                            aria-haspopup="menu"
+                            aria-expanded={isOpen}
                         >
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/type.tsx (1)

24-39: Re-evaluate default/resets for flex properties to avoid unexpected layout shifts.

  • Switching to flex: setting align-items: flex-start diverges from the CSS initial of stretch. This can change cross-axis sizing unexpectedly when users simply toggle “Flex”.
  • Switching away from flex: writing initial values persists explicit CSS that may override theme/cascade later. Often, not writing the property at all is preferable to “reset”.

Option A — Minimal, preserve CSS defaults:

-                    if (newValue === 'flex') {
-                        // When switching to flex, set default flex properties
-                        editorEngine.style.updateMultiple({
-                            display: 'flex',
-                            'flex-direction': 'row',
-                            'align-items': 'flex-start',
-                            'justify-content': 'flex-start',
-                        });
-                    } else {
-                        // When switching away from flex, clear flex properties
-                        editorEngine.style.update('display', newValue);
-                        // Clear flex-specific properties by setting them to initial values
-                        editorEngine.style.update('flex-direction', 'initial');
-                        editorEngine.style.update('align-items', 'initial');
-                        editorEngine.style.update('justify-content', 'initial');
-                    }
+                    if (newValue === 'flex') {
+                        // Let CSS defaults apply: flex-direction=row, align-items=stretch, justify-content=flex-start
+                        editorEngine.style.update('display', 'flex');
+                    } else {
+                        // Only change the display; avoid persisting explicit resets.
+                        editorEngine.style.update('display', newValue);
+                    }

Option B — If explicit values are required by the engine:

  • For “to flex”, set align-items: stretch (matches CSS default) and omit justify-content since default is already flex-start.
  • For “from flex”, prefer removing properties (if supported by the API) instead of writing initial. For example, editorEngine.style.updateMultiple({ 'flex-direction': undefined, 'align-items': undefined, 'justify-content': undefined }) or a dedicated clear API.

Please confirm how editorEngine.style treats absent vs. 'initial' values so we can finalize the safest variant.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6bb58 and d56c392.

📒 Files selected for processing (27)
  • apps/web/client/src/app/_components/hero/create.tsx (1 hunks)
  • apps/web/client/src/app/_components/hero/index.tsx (2 hunks)
  • apps/web/client/src/app/_components/shared/mockups/tailwind-color-editor.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/border-color.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/border.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/align.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/direction.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/gap.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/type.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/margin.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/opacity.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/padding.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/radius.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/index.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/inputs/input-image.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/overflow-menu.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/advanced-typography.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/font/font-size.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/text-align.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/device-selector.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/rotate-group.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/theme-group.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/window-actions-group.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/color-panel/index.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (2 hunks)
  • apps/web/client/src/components/ui/avatar-dropdown/plans.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
apps/web/client/src/components/ui/avatar-dropdown/plans.tsx (2)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
packages/stripe/src/constants.ts (1)
  • FREE_PRODUCT_CONFIG (46-52)
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/type.tsx (2)
apps/web/client/src/app/project/[id]/_components/editor-bar/inputs/input-radio.tsx (1)
  • InputRadio (23-52)
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/index.tsx (1)
  • layoutTypeOptions (23-27)
apps/web/client/src/app/project/[id]/_components/editor-bar/overflow-menu.tsx (2)
apps/web/client/src/app/project/[id]/_components/editor-bar/hover-tooltip.tsx (1)
  • HoverOnlyTooltip (14-35)
apps/web/client/src/app/project/[id]/_components/editor-bar/toolbar-button.tsx (1)
  • ToolbarButton (13-67)
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/index.tsx (1)
packages/ui/src/components/dropdown-menu.tsx (1)
  • DropdownMenuContent (216-216)
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/gap.tsx (1)
apps/web/client/src/app/project/[id]/_components/editor-bar/inputs/input-icon.tsx (1)
  • InputIcon (31-75)
apps/web/client/src/app/_components/shared/mockups/tailwind-color-editor.tsx (2)
apps/web/client/src/app/project/[id]/_components/editor-bar/toolbar-button.tsx (1)
  • ToolbarButton (13-67)
apps/web/client/src/app/project/[id]/_components/editor-bar/separator.tsx (1)
  • InputSeparator (1-3)
apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/theme-group.tsx (2)
apps/web/client/src/app/project/[id]/_components/editor-bar/toolbar-button.tsx (1)
  • ToolbarButton (13-67)
apps/web/client/src/app/project/[id]/_components/editor-bar/hover-tooltip.tsx (1)
  • HoverOnlyTooltip (14-35)
apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/rotate-group.tsx (3)
apps/web/client/src/components/store/editor/frames/manager.ts (1)
  • FrameData (11-15)
apps/web/client/src/app/project/[id]/_components/editor-bar/hover-tooltip.tsx (1)
  • HoverOnlyTooltip (14-35)
apps/web/client/src/app/project/[id]/_components/editor-bar/toolbar-button.tsx (1)
  • ToolbarButton (13-67)
apps/web/client/src/app/_components/hero/index.tsx (2)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/app/_components/hero/unicorn-background.tsx (1)
  • UnicornBackground (73-145)
apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/device-selector.tsx (2)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (9-13)
apps/web/client/src/app/project/[id]/_components/editor-bar/hover-tooltip.tsx (1)
  • HoverOnlyTooltip (14-35)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (4)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (9-13)
apps/web/client/src/components/store/editor/theme/index.ts (2)
  • colorGroups (618-620)
  • colorDefaults (622-624)
apps/web/client/src/components/store/editor/sandbox/index.ts (2)
  • handleFileChange (414-483)
  • index (77-131)
apps/web/client/src/components/store/editor/sandbox/file-event-bus.ts (1)
  • unsubscribe (34-36)
🪛 Biome (2.1.2)
apps/web/client/src/app/_components/hero/index.tsx

[error] 45-45: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".

Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.

(lint/security/noBlankTarget)

🔇 Additional comments (16)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (1)

114-128: Confirm subscribe() returns an unsubscribe function; otherwise switch to explicit unsubscribe API

The cleanup calls unsubscribe() assuming the bus returns a disposer. If subscribe doesn’t return one, use the bus’s unsubscribe(eventType, callback) API instead.

If needed, keep a reference to the callback and swap cleanup:

const cb = (event: FileEvent) => { /* ... */ };
editorEngine.sandbox.fileEventBus.subscribe('*', cb);

return () => {
    editorEngine.sandbox.fileEventBus.unsubscribe('*', cb);
};
apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/border-color.tsx (1)

46-51: Width tweak looks safe; verify hit area on touch devices

Reducing to min-w-9 keeps visual consistency with the rest of the bar; just double-check that 36px min width still meets your target tap area on smaller touch screens.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/margin.tsx (1)

154-160: LGTM on min-width reduction

The trigger remains legible with icon + value while aligning with the toolbar’s tighter spacing.

apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/advanced-typography.tsx (1)

56-60: LGTM on min-width reduction

Icon-only trigger with min-w-9 and px-2 remains balanced and consistent with adjacent controls.

apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/font/font-size.tsx (1)

98-104: Width tweak makes sense; confirm 3-digit values fit without truncation.

Moving to w-11 (≈44px) with min-w-[40px] gives the input enough room for typical 2–3 digit sizes and aligns closely with the 48px dropdown. Looks good.

Please sanity-check that values like 100 and 128 don’t visually clip in your default font and scaling.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/direction.tsx (1)

32-34: Nit: consistent label width and spacing look good.

Moving to gap-0 and w-20 aligns with the other display controls. No issues spotted.

apps/web/client/src/app/project/[id]/_components/editor-bar/window-selected/rotate-group.tsx (1)

12-16: Confirm state reactivity on direct dimension mutation.

If frameData.frame.dimension isn’t observable or doesn’t trigger a render/update, the UI may not reflect the swap. Ensure these assignments notify the relevant store/engine, or route through an action that does so.

Would you like me to scan for the frame/engine mutation helpers and propose a small wrapper (e.g., rotateFrame(frameData))?

apps/web/client/src/components/ui/avatar-dropdown/plans.tsx (2)

58-68: Nice loading UX with skeletons.

The loading placeholders for product/price and usage are clear and consistent with the rest of the UI.

Also applies to: 71-81


37-44: Verify scheduledChangeAt is a Date.

toLocaleDateString will throw if scheduledChangeAt is a string or undefined. If the API returns an ISO string, coerce via new Date(...) or adjust the tRPC transformer to hydrate Dates.

If needed, I can add a safe formatter util (handles undefined and string/Date input) and wire it here.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/border.tsx (1)

79-79: Width tweak LGTM; just watch content truncation.

The drop from min-w-10 to min-w-9 looks good and aligns with the PR’s tightening. Since this trigger can also show a numeric value, keep an eye on wrapping/truncation at smaller zoom levels.

Quick check: open Border with a 3-digit value (e.g., 128) and confirm it doesn’t wrap in common app widths.

apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/text-align.tsx (1)

36-36: LGTM on min-width reduction.

The switch to min-w-9 keeps the trigger compact without affecting icon hit area.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/radius.tsx (1)

109-109: LGTM; consistent with other toolbar triggers.

min-w-9 aligns with adjacent controls. The “Full” label still reads clearly at this width.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/opacity.tsx (1)

68-71: Good cleanup: remove fixed height to rely on ToolbarButton’s h-9 baseline.

This removes a potential height conflict and keeps trigger sizing consistent across the toolbar.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/index.tsx (1)

49-49: Consistent trigger width.

Using min-w-9 aligns this trigger with other toolbar buttons and helps prevent layout jitter.

apps/web/client/src/app/project/[id]/_components/editor-bar/overflow-menu.tsx (1)

27-37: Nice UX polish: tooltip on closed state only.

Wrapping the trigger with HoverOnlyTooltip and disabling it when open avoids overlapping popovers and keeps hints discoverable.

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/type.tsx (1)

17-19: Layout spacing/readability improvement LGTM.

Reducing to gap-0 and tightening the label to w-20 aligns with the rest of the display controls.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
<Icons.ArrowRight className="w-4 h-4" />
</a>
<a
<a
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a duplicate opening <a> tag on line 45 that creates invalid HTML. This will cause rendering issues and potential accessibility problems. The duplicate tag should be removed, keeping only the properly formed anchor element that follows it.

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/client/src/app/_components/hero/index.tsx (1)

44-49: External link now includes rel="noopener noreferrer" — resolved.

This addresses the earlier security/lint warning for target="_blank".

🧹 Nitpick comments (2)
apps/web/client/src/app/_components/hero/index.tsx (2)

21-30: Prefer matchMedia over resize listener to reduce work and avoid unnecessary rerenders.

The resize handler fires continuously while the user resizes, which can cause many state updates. A media query listener is cheaper and semantically matches the intent. Also consider extracting the 700 value into a constant to avoid a magic number.

Apply this diff inside the effect:

-    useEffect(() => {
-        const checkScreenHeight = () => {
-            setIsShortScreen(window.innerHeight < 700);
-        };
-        
-        checkScreenHeight();
-        window.addEventListener('resize', checkScreenHeight);
-        
-        return () => window.removeEventListener('resize', checkScreenHeight);
-    }, []);
+    useEffect(() => {
+        const mql = window.matchMedia('(max-height: 700px)');
+        // Initialize from current state
+        setIsShortScreen(mql.matches);
+        const handleChange = (e: MediaQueryListEvent) => setIsShortScreen(e.matches);
+        if ('addEventListener' in mql) {
+            mql.addEventListener('change', handleChange);
+            return () => mql.removeEventListener('change', handleChange);
+        } else {
+            // Safari < 14 fallback
+            // @ts-ignore older API
+            mql.addListener(handleChange);
+            // @ts-ignore older API
+            return () => mql.removeListener(handleChange);
+        }
+    }, []);

Optionally, define a constant near the component:

const SHORT_SCREEN_MAX_HEIGHT = 700; // px

And reference it in the query string.


52-53: Optional a11y: ensure decorative icon is hidden from screen readers.

If Icons.ArrowRight isn’t already aria-hidden internally, add aria-hidden="true" on the icon or provide an explicit aria-label on the link. The link text is clear, so hiding the icon is preferred.

-                                <Icons.ArrowRight className="w-4 h-4" />
+                                <Icons.ArrowRight className="w-4 h-4" aria-hidden="true" />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d56c392 and 9e4fb89.

📒 Files selected for processing (1)
  • apps/web/client/src/app/_components/hero/index.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/client/src/app/_components/hero/index.tsx (1)
apps/web/client/src/app/_components/hero/unicorn-background.tsx (1)
  • UnicornBackground (73-145)
🪛 Biome (2.1.2)
apps/web/client/src/app/_components/hero/index.tsx

[error] 46-46: expected , but instead found href

Remove href

(parse)


[error] 46-46: expected , but instead found =

Remove =

(parse)


[error] 47-47: expected , but instead found target

Remove target

(parse)


[error] 47-47: expected , but instead found =

Remove =

(parse)


[error] 48-48: expected , but instead found rel

Remove rel

(parse)


[error] 48-48: expected , but instead found =

Remove =

(parse)


[error] 49-49: expected , but instead found className

Remove className

(parse)


[error] 49-49: expected , but instead found =

Remove =

(parse)


[error] 51-51: unterminated string literal

The closing quote must be on the same line.

(parse)


[error] 52-52: expected > but instead found <

Remove <

(parse)

🔇 Additional comments (3)
apps/web/client/src/app/_components/hero/index.tsx (3)

6-6: Import useEffect alongside useState — looks good.

Correctly scoped to a client component and only used inside effects.


18-18: Adds isShortScreen state — OK.

Clear naming, boolean, and local to this component.


36-36: Minor spacing tweak — OK.

The added pt-8 aligns with the surrounding layout and motion timings.

value={value}
onChange={(newValue) => {
setValue(newValue);
editorEngine.style.update('display', newValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure we want to do this. If we want to switch back and forth don't we want to retain the existing flex setup?

@vercel vercel bot temporarily deployed to Preview – docs August 25, 2025 20:56 Inactive
@Kitenite Kitenite merged commit 6e072e5 into main Aug 25, 2025
4 checks passed
@Kitenite Kitenite deleted the styling-for-demo branch August 25, 2025 20:56
Comment on lines +76 to +109
setTimeout(async () => {
try {
await editorEngine.theme.scanConfig();

const { colorGroups, colorDefaults } = editorEngine.theme;

// Extract color-500 variants from project colors
const projectColors: string[] = [];

// Add colors from custom color groups (user-defined in Tailwind config)
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each custom color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});

// Add colors from default color groups (standard Tailwind colors)
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each default color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});

setBrandColors(projectColors);
} catch (error) {
console.warn('Theme scanning failed:', error);
}
}, 100); // Small delay to ensure file is fully written
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential memory leak in file change handler

The async operations inside setTimeout could lead to state updates after component unmount. If the component unmounts while the theme scanning is in progress, setBrandColors(projectColors) will attempt to update state on an unmounted component.

Consider adding a mount status check:

const isMounted = useRef(true);

useEffect(() => {
  return () => {
    isMounted.current = false;
  };
}, []);

// Then in the setTimeout callback:
setTimeout(async () => {
  try {
    await editorEngine.theme.scanConfig();
    // ...processing logic...
    
    if (isMounted.current) {
      setBrandColors(projectColors);
    }
  } catch (error) {
    console.warn('Theme scanning failed:', error);
  }
}, 100);

This pattern ensures state updates only occur when the component is still mounted, preventing memory leaks and React warnings about updates on unmounted components.

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +37 to +68
const loadBrandColors = async () => {
await editorEngine.theme.scanConfig();
const { colorGroups, colorDefaults } = editorEngine.theme;

// Extract color-500 variants from project colors
const projectColors: string[] = [];

// Add colors from custom color groups (user-defined in Tailwind config)
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each custom color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});

// Add colors from default color groups (standard Tailwind colors)
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each default color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});

setBrandColors(projectColors);
};

loadBrandColors();
}, [editorEngine.theme]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential Duplicate Colors in Brand Panel

The code processes both colorGroups and colorDefaults with identical logic, extracting colors with the same name criteria ('500', 'default', or 'DEFAULT'). This approach could lead to duplicate colors in the projectColors array if the same color appears in both collections.

Consider adding a deduplication step before setting the state:

// Remove duplicates before setting state
const uniqueColors = [...new Set(projectColors)];
setBrandColors(uniqueColors);

Alternatively, if maintaining the order is important, a more explicit deduplication approach could be used to ensure the brand color palette displays each color only once.

Suggested change
const loadBrandColors = async () => {
await editorEngine.theme.scanConfig();
const { colorGroups, colorDefaults } = editorEngine.theme;
// Extract color-500 variants from project colors
const projectColors: string[] = [];
// Add colors from custom color groups (user-defined in Tailwind config)
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each custom color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
// Add colors from default color groups (standard Tailwind colors)
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each default color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
setBrandColors(projectColors);
};
loadBrandColors();
}, [editorEngine.theme]);
const loadBrandColors = async () => {
await editorEngine.theme.scanConfig();
const { colorGroups, colorDefaults } = editorEngine.theme;
// Extract color-500 variants from project colors
const projectColors: string[] = [];
// Add colors from custom color groups (user-defined in Tailwind config)
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each custom color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
// Add colors from default color groups (standard Tailwind colors)
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each default color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
// Remove duplicates before setting state
const uniqueColors = [...new Set(projectColors)];
setBrandColors(uniqueColors);
};
loadBrandColors();
}, [editorEngine.theme]);

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +82 to +103
// Extract color-500 variants from project colors
const projectColors: string[] = [];

// Add colors from custom color groups (user-defined in Tailwind config)
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each custom color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});

// Add colors from default color groups (standard Tailwind colors)
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each default color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Code Duplication in Color Extraction Logic

The color extraction logic is duplicated between the loadBrandColors function and the file change handler. Both sections contain identical code for processing colorGroups and colorDefaults with the same conditions.

Consider extracting this logic into a separate function:

function extractBrandColors(colorGroups, colorDefaults) {
  const projectColors = [];
  
  // Process custom color groups
  Object.values(colorGroups).forEach(group => {
    group.forEach(color => {
      if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
        projectColors.push(color.lightColor);
      }
    });
  });
  
  // Process default color groups
  Object.values(colorDefaults).forEach(group => {
    group.forEach(color => {
      if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
        projectColors.push(color.lightColor);
      }
    });
  });
  
  return projectColors;
}

This would improve maintainability and ensure consistent behavior across both code paths.

Suggested change
// Extract color-500 variants from project colors
const projectColors: string[] = [];
// Add colors from custom color groups (user-defined in Tailwind config)
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each custom color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
// Add colors from default color groups (standard Tailwind colors)
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
// Get the default/500 color from each default color group
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
projectColors.push(color.lightColor);
}
});
});
// Extract color-500 variants from project colors
const projectColors = extractBrandColors(colorGroups, colorDefaults);
/**
* Extracts brand colors from color groups and defaults
* @param colorGroups - Custom color groups from Tailwind config
* @param colorDefaults - Default Tailwind color groups
* @returns Array of extracted brand colors
*/
function extractBrandColors(colorGroups, colorDefaults) {
const colors = [];
// Process custom color groups
Object.values(colorGroups).forEach(group => {
group.forEach(color => {
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
colors.push(color.lightColor);
}
});
});
// Process default color groups
Object.values(colorDefaults).forEach(group => {
group.forEach(color => {
if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
colors.push(color.lightColor);
}
});
});
return colors;
}

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (2)

70-112: Debounce and add retry/backoff for file-change rescans (race-proof the 100ms timer).

A fixed 100ms delay is fragile when editors write files in bursts. Debounce changes and add bounded retries with backoff to avoid scanning partial files.

-        const handleFileChange = async (filePath: string) => {
+        let debounceId: ReturnType<typeof setTimeout> | null = null;
+        const rescanWithRetry = (retries = 3) => {
+            const attempt = async () => {
+                try {
+                    await editorEngine.theme.scanConfig();
+                    setBrandColors(extractBrandColors(editorEngine.theme));
+                } catch (error) {
+                    if (retries > 0) {
+                        setTimeout(() => rescanWithRetry(retries - 1), 500);
+                    } else {
+                        console.error('Theme scanning failed after maximum retries:', error);
+                    }
+                }
+            };
+            void attempt();
+        };
+
+        const handleFileChange = async (filePath: string) => {
             // Check if the changed file is the Tailwind config
             if (filePath.includes('tailwind.config') || filePath.includes('globals.css')) {
-                // Rescan for new color groups
-                setTimeout(async () => {
-                    try {
-                        await editorEngine.theme.scanConfig();
-
-                        const { colorGroups, colorDefaults } = editorEngine.theme;
-
-                        // Extract color-500 variants from project colors
-                        const projectColors: string[] = [];
-
-                        // Add colors from custom color groups (user-defined in Tailwind config)
-                        Object.values(colorGroups).forEach(group => {
-                            group.forEach(color => {
-                                // Get the default/500 color from each custom color group
-                                if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
-                                    projectColors.push(color.lightColor);
-                                }
-                            });
-                        });
-
-                        // Add colors from default color groups (standard Tailwind colors)
-                        Object.values(colorDefaults).forEach(group => {
-                            group.forEach(color => {
-                                // Get the default/500 color from each default color group
-                                if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
-                                    projectColors.push(color.lightColor);
-                                }
-                            });
-                        });
-
-                        setBrandColors(projectColors);
-                    } catch (error) {
-                        console.warn('Theme scanning failed:', error);
-                    }
-                }, 100); // Small delay to ensure file is fully written
+                // Debounce to coalesce rapid successive writes and then retry/backoff.
+                if (debounceId) clearTimeout(debounceId);
+                debounceId = setTimeout(() => rescanWithRetry(3), 500);
             }
         };

Also clear debounceId in the effect cleanup (see next comment).


114-128: Subscription cleanup: don’t assume subscribe() returns a function; unsubscribe explicitly and guard cleanup.

The event bus exposes unsubscribe(eventType, callback). Prefer explicit unsubscribe to avoid runtime errors if subscribe returns void, and clear any pending debounce timer.

-        // Listen for file changes in the sandbox
-        const unsubscribe = editorEngine.sandbox.fileEventBus.subscribe('*', (event) => {
+        // Listen for file changes in the sandbox
+        const onEvent = (event: any) => {
             // Check if any of the changed files are Tailwind config files
             const isTailwindConfigChange = event.paths.some(path =>
                 path.includes('tailwind.config') || path.includes('globals.css')
             );
 
             if (isTailwindConfigChange && event.paths[0]) {
                 handleFileChange(event.paths[0]);
             }
-        });
+        };
+        editorEngine.sandbox.fileEventBus.subscribe('*', onEvent);
 
         return () => {
-            unsubscribe();
+            try {
+                // Clear pending debounced rescan if present
+                // (debounceId is declared in the effect above)
+                // @ts-ignore - exists when the debounced variant is adopted
+                if (typeof debounceId !== 'undefined' && debounceId) {
+                    clearTimeout(debounceId);
+                }
+                editorEngine.sandbox.fileEventBus.unsubscribe('*', onEvent);
+            } catch {
+                // no-op
+            }
         };

If you prefer the “subscribe returns unsubscribe” pattern, at least null-check before invoking as suggested in the previous review.

🧹 Nitpick comments (6)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (6)

18-18: Nit: Cursor pointer on child without handler.

The grid container handles onClick, so the cursor style on ColorSquare is redundant and slightly misleading if the square is used elsewhere. Consider dropping it or wiring the click at the square level.

-        className="w-full aspect-square cursor-pointer"
+        className="w-full aspect-square"

25-25: State addition looks good; consider distinguishing loading vs. empty.

Right now an empty array renders the shimmer placeholders indefinitely (also when no colors exist). Add a separate isLoading flag so “no colors found” can be represented distinctly.

-    const [brandColors, setBrandColors] = useState<string[]>([]);
+    const [brandColors, setBrandColors] = useState<string[]>([]);
+    const [isLoadingColors, setIsLoadingColors] = useState<boolean>(true);

And set isLoadingColors=false after successful/failed loads; use it in the render condition.


31-33: Avoid unhandled promise rejections in pre-load scan.

scanConfig likely returns a Promise. Wrap with a catch to prevent unhandled rejections and to aid debugging. Also confirm this pre-load isn’t redundant with the subsequent loadBrandColors effect that scans again.

-        editorEngine.theme.scanConfig();
+        void editorEngine.theme.scanConfig().catch((e) =>
+            console.warn('Preload theme scan failed:', e)
+        );

37-65: Deduplicate color extraction logic and harden against malformed groups.

This block is repeated below in the file-change handler, and it assumes array shapes and present lightColor. Extract a helper, dedupe values, slice to 12 (since UI shows 12 columns), and add error handling. Also set a loading state when starting/finishing.

-        const loadBrandColors = async () => {
-            await editorEngine.theme.scanConfig();
-            const { colorGroups, colorDefaults } = editorEngine.theme;
-
-            // Extract color-500 variants from project colors
-            const projectColors: string[] = [];
-
-            // Add colors from custom color groups (user-defined in Tailwind config)
-            Object.values(colorGroups).forEach(group => {
-                group.forEach(color => {
-                    // Get the default/500 color from each custom color group
-                    if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
-                        projectColors.push(color.lightColor);
-                    }
-                });
-            });
-
-            // Add colors from default color groups (standard Tailwind colors)
-            Object.values(colorDefaults).forEach(group => {
-                group.forEach(color => {
-                    // Get the default/500 color from each default color group
-                    if (color.name === '500' || color.name === 'default' || color.name === 'DEFAULT') {
-                        projectColors.push(color.lightColor);
-                    }
-                });
-            });
-
-            setBrandColors(projectColors);
-        };
+        const loadBrandColors = async () => {
+            try {
+                // optional: setIsLoadingColors?.(true);
+                await editorEngine.theme.scanConfig();
+                setBrandColors(extractBrandColors(editorEngine.theme));
+            } catch (e) {
+                console.warn('Failed to load brand colors:', e);
+                setBrandColors([]);
+            } finally {
+                // optional: setIsLoadingColors?.(false);
+            }
+        };

Additional helper (place at module scope):

function extractBrandColors(theme: any): string[] {
    const pick = (group: any): string[] => {
        if (!Array.isArray(group)) return [];
        const out: string[] = [];
        for (const color of group) {
            const name = String(color?.name ?? '').toLowerCase();
            if (name === '500' || name === 'default') {
                const v = color?.lightColor;
                if (typeof v === 'string' && v.trim()) out.push(v);
            }
        }
        return out;
    };
    const values: string[] = [];
    Object.values(theme.colorGroups || {}).forEach((g: any) => values.push(...pick(g)));
    Object.values(theme.colorDefaults || {}).forEach((g: any) => values.push(...pick(g)));
    return Array.from(new Set(values)).slice(0, 12);
}

149-151: Tailwind class conflict on hover border color.

Both hover:border-white and hover:border-transparent are present; the latter wins due to order, making the former dead code. Remove the conflicting class and keep the intended hover behavior.

-                        className="grid grid-cols-12 gap-0 rounded-lg overflow-hidden h-[40px] max-h-[40px] bg-background-onlook border-[0.5px] border-white/50 hover:border-[0.5px] hover:border-white cursor-pointer hover:border-transparent transition-all duration-200"
+                        className="grid grid-cols-12 gap-0 rounded-lg overflow-hidden h-[40px] max-h-[40px] bg-background-onlook border-[0.5px] border-white/50 hover:border-[0.5px] hover:border-transparent cursor-pointer transition-all duration-200"

If the intent was to emphasize the border on hover, keep hover:border-white and drop hover:border-transparent instead.


153-164: Constrain preview to 12 swatches and prefer stable keys; handle “no colors” vs. “loading”.

  • Use slice(0, 12) to avoid clipped overflow when more colors are available.
  • Prefer color-derived keys when possible; fallback to index to avoid duplicates.
  • Render placeholders only when loading (see isLoadingColors suggestion).
-                        {brandColors.length > 0 ? (
-                            brandColors.map((color, index) => (
-                                <ColorSquare key={`brand-color-${index}`} color={color} />
-                            ))
-                        ) : (
-                            Array.from({ length: 12 }, (_, index) => (
+                        {brandColors.length > 0 ? (
+                            brandColors.slice(0, 12).map((color, index) => (
+                                <ColorSquare key={`brand-color-${color}-${index}`} color={color} />
+                            ))
+                        ) : (
+                            Array.from({ length: 12 }, (_, index) => (
                                 <div
                                     key={`loading-color-${index}`}
                                     className="w-full h-full bg-gradient-to-r from-gray-300 via-gray-400 to-gray-300 bg-[length:200%_100%] animate-shimmer"
                                 />
                             ))
                         )}

Also consider showing a small “No brand colors detected” message instead of shimmer when not loading and brandColors.length === 0.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4fb89 and bccebcb.

📒 Files selected for processing (3)
  • apps/web/client/src/app/_components/hero/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/type.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/display/type.tsx
  • apps/web/client/src/app/_components/hero/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (4)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (9-13)
apps/web/client/src/components/store/editor/theme/index.ts (2)
  • colorGroups (618-620)
  • colorDefaults (622-624)
apps/web/client/src/components/store/editor/sandbox/index.ts (2)
  • handleFileChange (414-483)
  • index (77-131)
apps/web/client/src/components/store/editor/sandbox/file-event-bus.ts (1)
  • unsubscribe (34-36)
🔇 Additional comments (2)
apps/web/client/src/app/project/[id]/_components/left-panel/brand-tab/index.tsx (2)

5-5: LGTM on hooks import.

Importing useEffect/useState is appropriate for the added reactive behavior.


159-163: Verify animate-shimmer utility exists in Tailwind config.

The skeleton relies on a custom animate-shimmer class. If this utility isn’t defined, the effect won’t show. Confirm it exists in your Tailwind plugin setup.

@coderabbitai coderabbitai bot mentioned this pull request Sep 26, 2025
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants