refactor(webui): marketing home redesign from .pen — new sections, dark mode, animations#1730
Conversation
…rk mode, animations
Bring the marketing home page in line with the latest frontpages.pen design:
- Hero: new copy ("All the Power of AI. None of the Risk."), light/dark image
swap, two CTAs (request demo + read docs), full-bleed image with 500px
height cap and top-aligned crop
- New "Your Infrastructure. Your Data. Your Rules." section with 3
self-hosted/security/open-source pillar cards (animated SVGs per theme)
- Replaced FeatureSecure tab UI with 4 numbered feature blocks (Chat,
Conversations, Agents, Automations) — 2-column text + screenshot, theme-aware
images, balanced 2-line titles
- New IntegrationsBar with auto-scrolling 9-logo marquee, edge mask, and
pause-on-hover
- ComplianceTrust rewritten as a 3-column card (eyebrow + title + badges /
Independent / Certified) with inset vertical dividers
- FAQ accordion: tighter title spacing, real ArrowRight icon on "Contact our
team", flat backgrounds, snappier expand transition
- CTA: "Get Started with Tale" with secondary "See how it works" button
- Footer: GitHub icon moved into brand column, single-line copyright merged,
segmented theme switcher (sliding indicator), language switcher no longer
shows flag in trigger, full-width inset dividers
- Marketing-only dark palette overrides in app/globals.css (bg-base #0f0f0f,
border-base #27272a) and re-enabled ThemeProvider with FOUC script
- Removed unused FeatureGrid/FeatureSectors/LogoWall blocks + their
translation keys; cleaned up the corresponding marketing PNGs from public/
Translations updated across en/de/fr; de-CH inherits from de.
📝 WalkthroughWalkthroughThis PR redesigns the Tale marketing website homepage and supporting infrastructure. It introduces dark theme support at the application root, adds new Tagline and IntegrationsBar sections, redesigns the hero and compliance/trust sections, removes the legacy FeatureGrid/FeatureSectors/LogoWall components, and extends the footer with customizable theme switcher variants and language flag toggles. Integration provider icons are added to support the new sections. All UI copy is updated across English, German, and French locales. The changes span the UI component library (accordion), web UI components (language/theme switcher, site footer), and the marketing website application. Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/webui/src/layout/theme-switcher.tsx`:
- Line 61: Remove unnecessary type assertions by using theme directly in calls
like SEGMENTED_ORDER.indexOf(theme), ICONS[theme], and ORDER.indexOf(theme)
(drop all `as Theme` casts), and add reduced-motion overrides by appending
`motion-reduce:transition-none` to the elements that currently rely on
transitions (the segmented indicator, option elements, and the checkmark element
that use `transition-transform`, `transition-colors`, or `transition-opacity`)
so users with reduced motion preferences get no transitions.
In `@services/web/app/components/blocks/cta-deploy.tsx`:
- Line 37: The heading in the CTA component (component: cta-deploy / JSX element
with className containing "text-fg-base text-[32px] ... whitespace-nowrap")
forces a single line and breaks localization; remove the "whitespace-nowrap"
utility (or replace it with "whitespace-normal") from that className so
translated titles can wrap on narrow viewports while keeping the existing text
size and tracking classes intact.
In `@services/web/app/components/blocks/feature-secure.tsx`:
- Around line 9-30: Replace the forbidden "as const" usages by declaring
explicit readonly types: create a FeatureConfig type (shape { key: FeatureKey;
light: string; dark: string }) and a FeatureKey union type (e.g., type
FeatureKey = 'chat' | 'conversations' | 'agents' | 'automations'), then type
FEATURES as readonly FeatureConfig[] and annotate easeOut as readonly [number,
number, number, number]; update the featureKey variable to be typed as
FeatureKey so its type is derived from the declared union rather than using "as
const" on the array.
In `@services/web/app/components/blocks/hero-headline.tsx`:
- Around line 79-94: The hero images are using raw <img> tags; import the Image
component from '`@/components/ui/image`' at the top of the file and replace both
bare <img> elements (the ones with src "/marketing/hero-light.png" and
"/marketing/hero-dark.png") with the Image component, preserving the same props
and classes (src, alt, aria-hidden, className with dark:hidden/dark:block,
loading behavior and non-draggable behavior—use the Image API or an onDragStart
handler if needed) so styling and accessibility remain identical.
In `@services/web/app/components/blocks/integrations-bar.tsx`:
- Around line 67-88: The marquee animation on the element using the
"marquee-track" class (style={{ animation: 'marquee 40s linear infinite' }})
doesn't respect prefers-reduced-motion; update the component to disable or pause
the animation when the user prefers reduced motion — either remove/omit the
inline animation when window.matchMedia('(prefers-reduced-motion:
reduce)').matches is true or (preferably) add a CSS rule using `@media`
(prefers-reduced-motion: reduce) that sets .marquee-track { animation: none;
animation-play-state: paused; } so LogoTile/LOGOS rendering remains identical
but the motion is suppressed for reduced-motion users.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 314d38e1-2d8e-46cc-a629-67f09f05c9c2
⛔ Files ignored due to path filters (66)
designs/web/Frame 21 2.pngis excluded by!**/*.pngdesigns/web/Frame 21.pngis excluded by!**/*.pngdesigns/web/Frame 211.pngis excluded by!**/*.pngdesigns/web/Frame 212.pngis excluded by!**/*.pngdesigns/web/Frame 22 2.pngis excluded by!**/*.pngdesigns/web/Frame 22.pngis excluded by!**/*.pngdesigns/web/Frame 23.pngis excluded by!**/*.pngdesigns/web/Frame 24 2.pngis excluded by!**/*.pngdesigns/web/Frame 24.pngis excluded by!**/*.pngdesigns/web/Group 61.pngis excluded by!**/*.pngdesigns/web/Group 626.pngis excluded by!**/*.pngdesigns/web/dark mode/tale-01-self-hosted-animated (2).svgis excluded by!**/*.svgdesigns/web/dark mode/tale-02-security-animated (2).svgis excluded by!**/*.svgdesigns/web/dark mode/tale-03-open-source-animated (2).svgis excluded by!**/*.svgdesigns/web/hero-light-image.pngis excluded by!**/*.pngdesigns/web/images/Group 60.pngis excluded by!**/*.pngdesigns/web/images/ISO 27001-test 1.pngis excluded by!**/*.pngdesigns/web/images/Pexels Photo by Andrea Piacquadio-1.pngis excluded by!**/*.pngdesigns/web/images/Pexels Photo by Andrea Piacquadio.pngis excluded by!**/*.pngdesigns/web/images/Rectangle 6665-1.pngis excluded by!**/*.pngdesigns/web/images/Rectangle 6665.pngis excluded by!**/*.pngdesigns/web/images/Rectangle 6666.pngis excluded by!**/*.pngdesigns/web/images/Rectangle 6667.pngis excluded by!**/*.pngdesigns/web/images/Rectangle 6668.pngis excluded by!**/*.pngdesigns/web/images/Rectangle 666ii5.pngis excluded by!**/*.pngdesigns/web/images/Rectkangle 6667.pngis excluded by!**/*.pngdesigns/web/images/Rkectangle 6666.pngis excluded by!**/*.pngdesigns/web/images/SOC2 Type 2-test 1-1.pngis excluded by!**/*.pngdesigns/web/images/SOC2 Type 2-test 1.pngis excluded by!**/*.pngdesigns/web/images/SOC2 Type 2jk-test 1.pngis excluded by!**/*.pngdesigns/web/images/Screenshot 2026-05-04 at 2.26.26 PM.pngis excluded by!**/*.pngdesigns/web/light mode/tale-01-self-hosted-animated.svgis excluded by!**/*.svgdesigns/web/light mode/tale-02-security-animated.svgis excluded by!**/*.svgdesigns/web/light mode/tale-03-open-source-animated (3).svgis excluded by!**/*.svgservices/web/public/marketing/feature-agents-dark.pngis excluded by!**/*.pngservices/web/public/marketing/feature-agents.pngis excluded by!**/*.pngservices/web/public/marketing/feature-automations-dark.pngis excluded by!**/*.pngservices/web/public/marketing/feature-automations.pngis excluded by!**/*.pngservices/web/public/marketing/feature-chat-dark.pngis excluded by!**/*.pngservices/web/public/marketing/feature-chat-panel.pngis excluded by!**/*.pngservices/web/public/marketing/feature-chat.pngis excluded by!**/*.pngservices/web/public/marketing/feature-conversations-dark.pngis excluded by!**/*.pngservices/web/public/marketing/feature-conversations.pngis excluded by!**/*.pngservices/web/public/marketing/feature-open-source-dark.svgis excluded by!**/*.svgservices/web/public/marketing/feature-open-source.svgis excluded by!**/*.svgservices/web/public/marketing/feature-secure-approvals.pngis excluded by!**/*.pngservices/web/public/marketing/feature-secure-automations.pngis excluded by!**/*.pngservices/web/public/marketing/feature-secure-chat.pngis excluded by!**/*.pngservices/web/public/marketing/feature-secure-conversations.pngis excluded by!**/*.pngservices/web/public/marketing/feature-security-dark.svgis excluded by!**/*.svgservices/web/public/marketing/feature-security.svgis excluded by!**/*.svgservices/web/public/marketing/feature-self-hosted-dark.svgis excluded by!**/*.svgservices/web/public/marketing/feature-self-hosted.svgis excluded by!**/*.svgservices/web/public/marketing/hero-chat.pngis excluded by!**/*.pngservices/web/public/marketing/hero-dark.pngis excluded by!**/*.pngservices/web/public/marketing/hero-light.pngis excluded by!**/*.pngservices/web/public/marketing/sector-finance.pngis excluded by!**/*.pngservices/web/public/marketing/sector-hospitality.pngis excluded by!**/*.pngservices/web/public/marketing/sector-legal.pngis excluded by!**/*.pngservices/web/public/marketing/sector-mock.pngis excluded by!**/*.pngservices/web/public/marketing/security-1-independent.pngis excluded by!**/*.pngservices/web/public/marketing/security-2-stack.pngis excluded by!**/*.pngservices/web/public/marketing/security-3-proven.pngis excluded by!**/*.pngservices/web/public/marketing/security-4-needs.pngis excluded by!**/*.pngservices/web/public/marketing/trust-blocks.pngis excluded by!**/*.pngservices/web/public/marketing/trust-network.pngis excluded by!**/*.png
📒 Files selected for processing (29)
designs/web/frontpages.penpackages/ui/src/components/feedback/accordion.tsxpackages/webui/src/layout/language-switcher.tsxpackages/webui/src/layout/site-footer.tsxpackages/webui/src/layout/theme-switcher.tsxservices/web/app/components/blocks/compliance-trust.tsxservices/web/app/components/blocks/cta-deploy.tsxservices/web/app/components/blocks/faq-accordion.tsxservices/web/app/components/blocks/feature-grid.tsxservices/web/app/components/blocks/feature-sectors.tsxservices/web/app/components/blocks/feature-secure.tsxservices/web/app/components/blocks/form-card.tsxservices/web/app/components/blocks/hero-headline.tsxservices/web/app/components/blocks/integrations-bar.tsxservices/web/app/components/blocks/logo-wall.tsxservices/web/app/components/blocks/segmented-radio.tsxservices/web/app/components/blocks/tagline.tsxservices/web/app/components/blocks/tier-card.tsxservices/web/app/components/icons/integration-icons.tsxservices/web/app/components/layout/site-footer.tsxservices/web/app/entry-server.tsxservices/web/app/globals.cssservices/web/app/main.tsxservices/web/app/pages/home-page.tsxservices/web/app/pages/legal-page.tsxservices/web/index.htmlservices/web/messages/de.jsonservices/web/messages/en.jsonservices/web/messages/fr.json
💤 Files with no reviewable changes (3)
- services/web/app/components/blocks/feature-grid.tsx
- services/web/app/components/blocks/logo-wall.tsx
- services/web/app/components/blocks/feature-sectors.tsx
| function SegmentedThemeSwitcher({ className }: { className?: string }) { | ||
| const { t } = useT('themeSwitcher'); | ||
| const { theme, setTheme } = useTheme(); | ||
| const activeIndex = Math.max(0, SEGMENTED_ORDER.indexOf(theme as Theme)); |
There was a problem hiding this comment.
Remove theme as Theme assertions and add reduced-motion overrides for the segmented transitions.
- Replace
SEGMENTED_ORDER.indexOf(theme as Theme)(and othertheme as Themecasts likeICONS[theme as Theme]/ORDER.indexOf(theme as Theme)) with directthemeusage—noasneeded. - Add
motion-reduce:transition-noneto the indicator/options/checkmark elements that currently usetransition-transform,transition-colors, andtransition-opacity.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/webui/src/layout/theme-switcher.tsx` at line 61, Remove unnecessary
type assertions by using theme directly in calls like
SEGMENTED_ORDER.indexOf(theme), ICONS[theme], and ORDER.indexOf(theme) (drop all
`as Theme` casts), and add reduced-motion overrides by appending
`motion-reduce:transition-none` to the elements that currently rely on
transitions (the segmented indicator, option elements, and the checkmark element
that use `transition-transform`, `transition-colors`, or `transition-opacity`)
so users with reduced motion preferences get no transitions.
| > | ||
| <h2 | ||
| className="text-accent-base text-[32px] font-medium tracking-[-0.044em] md:text-[56px] md:tracking-[-0.038em]" | ||
| className="text-fg-base text-[32px] font-medium tracking-[-0.044em] whitespace-nowrap md:text-[56px] md:tracking-[-0.038em]" |
There was a problem hiding this comment.
Avoid forcing one-line heading for localized CTA title.
Line 37 uses whitespace-nowrap, which can cause overflow/clipping for longer translations on narrower viewports. Let the heading wrap.
💡 Suggested change
- className="text-fg-base text-[32px] font-medium tracking-[-0.044em] whitespace-nowrap md:text-[56px] md:tracking-[-0.038em]"
+ className="text-fg-base text-[32px] font-medium tracking-[-0.044em] md:text-[56px] md:tracking-[-0.038em]"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className="text-fg-base text-[32px] font-medium tracking-[-0.044em] whitespace-nowrap md:text-[56px] md:tracking-[-0.038em]" | |
| className="text-fg-base text-[32px] font-medium tracking-[-0.044em] md:text-[56px] md:tracking-[-0.038em]" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/web/app/components/blocks/cta-deploy.tsx` at line 37, The heading in
the CTA component (component: cta-deploy / JSX element with className containing
"text-fg-base text-[32px] ... whitespace-nowrap") forces a single line and
breaks localization; remove the "whitespace-nowrap" utility (or replace it with
"whitespace-normal") from that className so translated titles can wrap on narrow
viewports while keeping the existing text size and tracking classes intact.
| const FEATURES = [ | ||
| { | ||
| key: 'chat', | ||
| light: '/marketing/feature-chat.png', | ||
| dark: '/marketing/feature-chat-dark.png', | ||
| }, | ||
| }); | ||
|
|
||
| // Frosted glass inner card overlaying the gradient panel. Mirrors the | ||
| // Pencil spec: flush-left, ~6% right margin, ~10% top inset, extends past | ||
| // the bottom edge so it's clipped by the parent. Rounded only at the | ||
| // top-right corner with a subtle gradient stroke (white/20 → white). | ||
| const panelGlassCard = | ||
| 'pointer-events-none absolute top-[10.4%] right-[6%] bottom-[-3.3%] left-0 overflow-hidden rounded-tr-3xl bg-white/10 backdrop-blur-md'; | ||
|
|
||
| // Gradient stroke for the glass card — top is fainter, bottom edge near | ||
| // fully opaque white. Implemented as a separate absolute ring so it sits | ||
| // on top of the backdrop-blurred surface. | ||
| const panelGlassStroke = | ||
| 'pointer-events-none absolute inset-0 rounded-tr-3xl [mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] [mask-composite:exclude] p-px [background:linear-gradient(180deg,rgba(255,255,255,0.2)_0%,rgba(255,255,255,1)_100%)]'; | ||
| { | ||
| key: 'conversations', | ||
| light: '/marketing/feature-conversations.png', | ||
| dark: '/marketing/feature-conversations-dark.png', | ||
| }, | ||
| { | ||
| key: 'agents', | ||
| light: '/marketing/feature-agents.png', | ||
| dark: '/marketing/feature-agents-dark.png', | ||
| }, | ||
| { | ||
| key: 'automations', | ||
| light: '/marketing/feature-automations.png', | ||
| dark: '/marketing/feature-automations-dark.png', | ||
| }, | ||
| ] as const; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="services/web/app/components/blocks/feature-secure.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
# Show the relevant sections with line numbers
nl -ba "$FILE" | sed -n '1,220p'Repository: tale-project/tale
Length of output: 105
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="services/web/app/components/blocks/feature-secure.tsx"
echo "== file head (with line numbers) =="
cat -n "$FILE" | sed -n '1,220p'
echo
echo "== search for 'as const' and ' as ' patterns =="
rg -n "as const|as [A-Za-z_]" "$FILE" || trueRepository: tale-project/tale
Length of output: 4299
Remove as const assertions in feature-secure.tsx
services/web/app/components/blocks/feature-secure.tsxusesas constfor botheaseOut(line 7) andFEATURES(line 30); repo rules forbidasassertions.- Replace
FEATURES: ... as constwith explicit readonly typing (e.g.,FeatureKey/FeatureConfig) and keepfeatureKeytyped from that declared type.
♻️ Proposed fix (FEATURES + featureKey)
-type FeatureKey = 'chat' | 'conversations' | 'agents' | 'automations';
+type FeatureKey = 'chat' | 'conversations' | 'agents' | 'automations';
-interface FeatureConfig {
+interface FeatureConfig {
key: FeatureKey;
light: string;
dark: string;
}
-const FEATURES: readonly FeatureConfig[] = [
+const FEATURES: readonly FeatureConfig[] = [
{
key: 'chat',
light: '/marketing/feature-chat.png',
dark: '/marketing/feature-chat-dark.png',
},
@@
-];
+];
interface FeatureRowProps {
- featureKey: (typeof FEATURES)[number]['key'];
+ featureKey: FeatureConfig['key'];
light: string;
dark: string;
index: number;
isLast: boolean;
}Also change easeOut to an explicit readonly tuple type (e.g., readonly [number, number, number, number]) without as.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/web/app/components/blocks/feature-secure.tsx` around lines 9 - 30,
Replace the forbidden "as const" usages by declaring explicit readonly types:
create a FeatureConfig type (shape { key: FeatureKey; light: string; dark:
string }) and a FeatureKey union type (e.g., type FeatureKey = 'chat' |
'conversations' | 'agents' | 'automations'), then type FEATURES as readonly
FeatureConfig[] and annotate easeOut as readonly [number, number, number,
number]; update the featureKey variable to be typed as FeatureKey so its type is
derived from the declared union rather than using "as const" on the array.
| <img | ||
| src="/marketing/hero-chat.png" | ||
| src="/marketing/hero-light.png" | ||
| alt="" | ||
| aria-hidden | ||
| className="mx-auto block max-h-125 w-full object-cover object-top select-none dark:hidden" | ||
| loading="eager" | ||
| draggable={false} | ||
| /> | ||
| <img | ||
| src="/marketing/hero-dark.png" | ||
| alt="" | ||
| aria-hidden | ||
| className="w-full select-none" | ||
| className="mx-auto hidden max-h-125 w-full object-cover object-top select-none dark:block" | ||
| loading="eager" | ||
| draggable={false} | ||
| /> |
There was a problem hiding this comment.
Replace bare <img> with the Image component.
Both hero images use bare <img> tags instead of the custom Image component. As per coding guidelines, all images must use the Image component from @/components/ui/image.
📸 Proposed fix using the Image component
First, add the import at the top of the file:
import { Button } from '`@tale/ui/button`';
+import { Image } from '`@tale/ui/image`';
import { motion, useReducedMotion } from 'framer-motion';Then replace the bare <img> tags:
- <img
+ <Image
src="/marketing/hero-light.png"
alt=""
aria-hidden
className="mx-auto block max-h-125 w-full object-cover object-top select-none dark:hidden"
loading="eager"
draggable={false}
/>
- <img
+ <Image
src="/marketing/hero-dark.png"
alt=""
aria-hidden
className="mx-auto hidden max-h-125 w-full object-cover object-top select-none dark:block"
loading="eager"
draggable={false}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <img | |
| src="/marketing/hero-chat.png" | |
| src="/marketing/hero-light.png" | |
| alt="" | |
| aria-hidden | |
| className="mx-auto block max-h-125 w-full object-cover object-top select-none dark:hidden" | |
| loading="eager" | |
| draggable={false} | |
| /> | |
| <img | |
| src="/marketing/hero-dark.png" | |
| alt="" | |
| aria-hidden | |
| className="w-full select-none" | |
| className="mx-auto hidden max-h-125 w-full object-cover object-top select-none dark:block" | |
| loading="eager" | |
| draggable={false} | |
| /> | |
| import { Button } from '`@tale/ui/button`'; | |
| import { Image } from '`@tale/ui/image`'; | |
| import { motion, useReducedMotion } from 'framer-motion'; |
| <img | |
| src="/marketing/hero-chat.png" | |
| src="/marketing/hero-light.png" | |
| alt="" | |
| aria-hidden | |
| className="mx-auto block max-h-125 w-full object-cover object-top select-none dark:hidden" | |
| loading="eager" | |
| draggable={false} | |
| /> | |
| <img | |
| src="/marketing/hero-dark.png" | |
| alt="" | |
| aria-hidden | |
| className="w-full select-none" | |
| className="mx-auto hidden max-h-125 w-full object-cover object-top select-none dark:block" | |
| loading="eager" | |
| draggable={false} | |
| /> | |
| <Image | |
| src="/marketing/hero-light.png" | |
| alt="" | |
| aria-hidden | |
| className="mx-auto block max-h-125 w-full object-cover object-top select-none dark:hidden" | |
| loading="eager" | |
| draggable={false} | |
| /> | |
| <Image | |
| src="/marketing/hero-dark.png" | |
| alt="" | |
| aria-hidden | |
| className="mx-auto hidden max-h-125 w-full object-cover object-top select-none dark:block" | |
| loading="eager" | |
| draggable={false} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/web/app/components/blocks/hero-headline.tsx` around lines 79 - 94,
The hero images are using raw <img> tags; import the Image component from
'`@/components/ui/image`' at the top of the file and replace both bare <img>
elements (the ones with src "/marketing/hero-light.png" and
"/marketing/hero-dark.png") with the Image component, preserving the same props
and classes (src, alt, aria-hidden, className with dark:hidden/dark:block,
loading behavior and non-draggable behavior—use the Image API or an onDragStart
handler if needed) so styling and accessibility remain identical.
| <div | ||
| role="list" | ||
| aria-label={t('integrations.title')} | ||
| className="group w-full overflow-hidden mask-[linear-gradient(to_right,transparent,#000_10%,#000_90%,transparent)]" | ||
| > | ||
| <div | ||
| className="marquee-track flex w-max items-center gap-6 group-hover:[animation-play-state:paused] md:gap-10" | ||
| style={{ animation: 'marquee 40s linear infinite' }} | ||
| > | ||
| {LOGOS.map(({ Icon, name }) => ( | ||
| <LogoTile key={`a-${name}`} Icon={Icon} name={name} /> | ||
| ))} | ||
| {LOGOS.map(({ Icon, name }) => ( | ||
| <LogoTile | ||
| key={`b-${name}`} | ||
| Icon={Icon} | ||
| name={name} | ||
| ariaHidden | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Marquee animation must respect prefers-reduced-motion.
The continuous marquee animation at line 74 does not respect the user's motion preferences. As per coding guidelines, all animations must honor prefers-reduced-motion. Users with vestibular disorders who enable this setting should see a static or paused state instead of continuous scrolling.
♿ Proposed fix to respect reduced motion
<div
role="list"
aria-label={t('integrations.title')}
className="group w-full overflow-hidden mask-[linear-gradient(to_right,transparent,`#000_10`%,`#000_90`%,transparent)]"
>
<div
className="marquee-track flex w-max items-center gap-6 group-hover:[animation-play-state:paused] md:gap-10"
- style={{ animation: 'marquee 40s linear infinite' }}
+ style={{
+ animation: reduceMotion
+ ? 'none'
+ : 'marquee 40s linear infinite',
+ }}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| role="list" | |
| aria-label={t('integrations.title')} | |
| className="group w-full overflow-hidden mask-[linear-gradient(to_right,transparent,#000_10%,#000_90%,transparent)]" | |
| > | |
| <div | |
| className="marquee-track flex w-max items-center gap-6 group-hover:[animation-play-state:paused] md:gap-10" | |
| style={{ animation: 'marquee 40s linear infinite' }} | |
| > | |
| {LOGOS.map(({ Icon, name }) => ( | |
| <LogoTile key={`a-${name}`} Icon={Icon} name={name} /> | |
| ))} | |
| {LOGOS.map(({ Icon, name }) => ( | |
| <LogoTile | |
| key={`b-${name}`} | |
| Icon={Icon} | |
| name={name} | |
| ariaHidden | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| <div | |
| role="list" | |
| aria-label={t('integrations.title')} | |
| className="group w-full overflow-hidden mask-[linear-gradient(to_right,transparent,`#000_10`%,`#000_90`%,transparent)]" | |
| > | |
| <div | |
| className="marquee-track flex w-max items-center gap-6 group-hover:[animation-play-state:paused] md:gap-10" | |
| style={{ | |
| animation: reduceMotion | |
| ? 'none' | |
| : 'marquee 40s linear infinite', | |
| }} | |
| > | |
| {LOGOS.map(({ Icon, name }) => ( | |
| <LogoTile key={`a-${name}`} Icon={Icon} name={name} /> | |
| ))} | |
| {LOGOS.map(({ Icon, name }) => ( | |
| <LogoTile | |
| key={`b-${name}`} | |
| Icon={Icon} | |
| name={name} | |
| ariaHidden | |
| /> | |
| ))} | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/web/app/components/blocks/integrations-bar.tsx` around lines 67 -
88, The marquee animation on the element using the "marquee-track" class
(style={{ animation: 'marquee 40s linear infinite' }}) doesn't respect
prefers-reduced-motion; update the component to disable or pause the animation
when the user prefers reduced motion — either remove/omit the inline animation
when window.matchMedia('(prefers-reduced-motion: reduce)').matches is true or
(preferably) add a CSS rule using `@media` (prefers-reduced-motion: reduce) that
sets .marquee-track { animation: none; animation-play-state: paused; } so
LogoTile/LOGOS rendering remains identical but the motion is suppressed for
reduced-motion users.
- theme-switcher: drop unnecessary `as Theme` casts, add `motion-reduce:transition-none` to the segmented indicator/option/checkmark transitions so reduced-motion users get a static UI - cta-deploy: change `whitespace-nowrap` to `md:whitespace-nowrap` so the hero CTA title can still wrap on narrow viewports for longer translations - feature-secure: replace `as const` on FEATURES + easeOut with explicit `FeatureKey` / `FeatureConfig` types + a typed readonly tuple - hero-headline: switch the two bare `<img>` tags to the shared `Image` component from `@tale/ui/image` Marquee already respects `prefers-reduced-motion` via the existing CSS rule in services/web/app/globals.css, so no change needed there.
Summary
Brings the marketing home page in line with the latest
frontpages.pendesign — new sections, dark-mode support, and subtle entrance/marquee animations across the page.What changed
Hero
DOCS_URL)max-h-125+object-cover object-topso it never stretches absurdly tall on wide screens or clips the mockup's top"Your Infrastructure. Your Data. Your Rules." (new section)
#fafafalight /#151515dark)Feature blocks (Chat / Conversations / Agents / Automations)
FeatureSecure— now a stacked 4-section block\nline breaks in translationsIntegrations bar (new)
Compliance & Trust
#fcfcfdlight /#141416darklg, stacks vertically with horizontal dividersFAQ
ArrowRighticon on "Contact our team" (withhover:underline)CTA
Footer
Dark mode
ThemeProviderinapp/main.tsx+entry-server.tsxindex.htmlapp/globals.css:--color-bg-base: #0f0f0f,--color-border-base: #27272aCleanup
FeatureGrid,FeatureSectors,LogoWallblocks + their translation keyspublic/marketing/Translations updated across en/de/fr; de-CH inherits from de.
Test plan
bun run checkpasses (verified locally — 41/41 green)/request-demo, Read docs →DOCS_URL)/contactSummary by CodeRabbit
New Features
UI Improvements
Other