improvement(home): redesign workspace home template experience#3880
improvement(home): redesign workspace home template experience#3880ananmaysharan wants to merge 1 commit intosimstudioai:mainfrom
Conversation
Replace vertical scroll-all template layout with category-based pill navigation. Adds responsive horizontal scrolling with gradient fade indicators on mobile, natural wrapping on desktop. Template cards now show integration icon stacks and anchor preview images from top-left.
|
@ananmaysharan is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
PR SummaryLow Risk Overview Template cards now show a right-aligned Template/category metadata is updated so Written by Cursor Bugbot for commit 5e4fac7. This will update automatically on new commits. Configure here. |
Greptile SummaryThis PR redesigns the workspace home template section, replacing a flat vertical-scroll layout with a category-based pill navigation, responsive horizontal scrolling on mobile, and template cards that now surface integration icon stacks alongside the card title. Key changes:
One P1 issue found: Confidence Score: 4/5Safe to merge after addressing the incorrect initial showRightFade state on mobile. One P1 visual defect (right gradient indicator appearing when there is no overflowing content on certain mobile viewports) should be fixed before merging. The two P2 findings are non-blocking style improvements. apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/template-prompts.tsx (showRightFade init), apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/components/integration-icon-stack.tsx (animate prop) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[TemplatePrompts] --> B{activeCategory}
B -->|popular| C[TEMPLATES filtered by featured]
B -->|other| D[TEMPLATES filtered by category]
C --> E[grid of TemplateCard]
D --> E
E --> F{template.image?}
F -->|yes| G[Next/Image anchored top-left]
F -->|no| H[TemplatePreview\ntable / knowledge / file / workflow]
E --> I[IntegrationIconStack\nhovering spring-scales icons]
A --> J[pill nav]
J --> K[scroll fade L/R indicators\nmobile only]
K --> L[handleNavScroll\nupdates showLeftFade / showRightFade]
Reviews (1): Last reviewed commit: "improvement(home): redesign workspace ho..." | Re-trigger Greptile |
| const [showLeftFade, setShowLeftFade] = useState(false) | ||
| const [showRightFade, setShowRightFade] = useState(true) |
There was a problem hiding this comment.
Right fade gradient shown incorrectly on initial render
showRightFade is initialised to true unconditionally. On mobile screens where all 7 category pills fit without overflowing (e.g. wider phones in landscape mode, ~430–767 px), the right gradient indicator is displayed even though there is no additional content to scroll to. The handleNavScroll handler is only called when the user scrolls, so the flag is never corrected after mount if the element does not overflow.
Add a useLayoutEffect (or useEffect) to run the scroll check once on mount:
| const [showLeftFade, setShowLeftFade] = useState(false) | |
| const [showRightFade, setShowRightFade] = useState(true) | |
| const [showLeftFade, setShowLeftFade] = useState(false) | |
| const [showRightFade, setShowRightFade] = useState(false) | |
| useLayoutEffect(() => { | |
| handleNavScroll() | |
| }, [handleNavScroll]) |
Also import useLayoutEffect at the top:
import { type ComponentType, memo, type SVGProps, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'| animate={{ | ||
| transform: `scale(${getScale(index)})`, | ||
| }} | ||
| transition={{ type: 'spring', bounce: 0.15, duration: 0.25 }} | ||
| style={{ zIndex: hoveredIndex === index ? 50 : icons.length - index }} | ||
| className='-ml-1 flex h-5 w-5 origin-bottom items-center justify-center rounded-full border border-[var(--border-1)] bg-[var(--white)] first:ml-0 dark:bg-[var(--surface-4)]' | ||
| > | ||
| <Icon aria-hidden='true' className='h-3 w-3' /> | ||
| </motion.div> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
Use individual framer-motion transform property instead of CSS string
Passing a raw CSS transform string to framer-motion's animate prop (transform: \scale(…)`) is not the recommended pattern. Framer-motion's interpolation engine works most reliably with individual transform keys (scale, x, y, etc.), not composite CSS strings. Using individual keys also gives you correct origin-bottom` behaviour from the Tailwind class already applied.
| animate={{ | |
| transform: `scale(${getScale(index)})`, | |
| }} | |
| transition={{ type: 'spring', bounce: 0.15, duration: 0.25 }} | |
| style={{ zIndex: hoveredIndex === index ? 50 : icons.length - index }} | |
| className='-ml-1 flex h-5 w-5 origin-bottom items-center justify-center rounded-full border border-[var(--border-1)] bg-[var(--white)] first:ml-0 dark:bg-[var(--surface-4)]' | |
| > | |
| <Icon aria-hidden='true' className='h-3 w-3' /> | |
| </motion.div> | |
| ))} | |
| </div> | |
| animate={{ scale: getScale(index) }} |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const INTEGRATION_BLOCK_ICONS: Record<string, IconComponent> = { | ||
| airtable: AirtableIcon, | ||
| amplitude: AmplitudeIcon, | ||
| apollo: ApolloIcon, | ||
| asana: AsanaIcon, | ||
| calendly: CalendlyIcon, | ||
| confluence: ConfluenceIcon, | ||
| datadog: DatadogIcon, | ||
| discord: DiscordIcon, | ||
| firecrawl: FirecrawlIcon, | ||
| github: GithubIcon, | ||
| gmail: GmailIcon, | ||
| gong: GongIcon, | ||
| google_calendar: GoogleCalendarIcon, | ||
| google_docs: GoogleDocsIcon, | ||
| google_drive: GoogleDriveIcon, | ||
| google_sheets: GoogleSheetsIcon, | ||
| greenhouse: GreenhouseIcon, | ||
| hubspot: HubspotIcon, | ||
| intercom: IntercomIcon, | ||
| jira: JiraIcon, | ||
| lemlist: LemlistIcon, | ||
| linear: LinearIcon, | ||
| linkedin: LinkedInIcon, | ||
| mailchimp: MailchimpIcon, | ||
| microsoft_teams: MicrosoftTeamsIcon, | ||
| notion: NotionIcon, | ||
| obsidian: ObsidianIcon, | ||
| pagerduty: PagerDutyIcon, | ||
| reddit: RedditIcon, | ||
| salesforce: SalesforceIcon, | ||
| sentry: SentryIcon, | ||
| shopify: ShopifyIcon, | ||
| slack: SlackIcon, | ||
| stripe: StripeIcon, | ||
| twilio_sms: TwilioIcon, | ||
| typeform: TypeformIcon, | ||
| webflow: WebflowIcon, | ||
| wordpress: WordpressIcon, | ||
| x: xIcon, | ||
| youtube: YouTubeIcon, | ||
| zendesk: ZendeskIcon, | ||
| } as const |
There was a problem hiding this comment.
Config map should live in a dedicated config file
Per the project's component convention, component configuration (like INTEGRATION_BLOCK_ICONS) should be placed in a dedicated .ts config file rather than inline in the component file. This also mirrors how the icon maps were previously organised in consts.ts.
Consider extracting it to a sibling file such as components/integration-icon-config.ts and importing the map into integration-icon-stack.tsx.
Rule Used: When defining properties for components, use a ded... (source)
Learnt From
simstudioai/sim#367
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| if (!el) return | ||
| setShowLeftFade(el.scrollLeft > 2) | ||
| setShowRightFade(el.scrollLeft < el.scrollWidth - el.clientWidth - 2) | ||
| }, []) |
There was a problem hiding this comment.
Right fade gradient shows when nav doesn't overflow
Low Severity
showRightFade is initialized to true, but it's only corrected by the handleNavScroll callback on onScroll events. If the category pills fit within the viewport without overflowing (e.g., wider mobile screens or landscape), no scroll event ever fires, so the right gradient fade indicator remains permanently visible despite there being nothing to scroll to. A useEffect (or ResizeObserver) that checks el.scrollWidth > el.clientWidth after mount and on resize is needed to set the correct initial state.


Summary
Screenshots/Videos
Before:
before.mp4
After:
after.mp4
Design Decisions
I chose to focus on redesigning the template card experience into a pill based navigation to help users quickly navigate and explore the existing pre-built templates that could be useful to them, based on already existing category variable within the templates. I focused on making it work across dark and light modes and different viewport sizes. In addition, I redesigned the card title to include all of the integrations a particular template uses, in order to show the breadth of capabilities and integrations Sim has, as well as for users looking for a particular integration to be able to scan the templates section for it quickly. Finally, I made some small fixes to layout, color and hover states in the
homepage.Next Steps
Given more time, my focus would go to the contents of the cards themselves. They are generic and repeated for many of the templates, and inconsistent light and dark backgrounds for the templates in the featured section. Future work could go into making the content dynamic and representative of each template exactly as well as showing more information about the types of modules as well as the integrations. Beyond that, adding some more interaction/details/animation into the card and pill hover states as well as diversifying the icon set used to make them more representative of the template purpose. Finally, there are some color contrast/accessibility issues with some logos in the integrations in dark/light mode, work is needed to ensure the use of correct color/monochrome logo versions for integrations in the right contexts.