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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function FeaturedCustomerCard({ story, active, emphasized }: FeaturedCust
<article
aria-hidden={!active}
className={cn(
'relative isolate size-full overflow-hidden [clip-path:border-box]',
'relative isolate size-full overflow-hidden [clip-path:inset(0_round_12px)]',
isFilm ? 'bg-black' : 'bg-[var(--surface-4)]',
LANDING_STAGE_RADIUS
)}
Expand All @@ -95,7 +95,7 @@ export function FeaturedCustomerCard({ story, active, emphasized }: FeaturedCust
fill
sizes='(max-width: 640px) 100vw, 1728px'
quality={90}
className='object-cover'
className='rounded-[inherit] object-cover'
/>
<video
ref={videoRef}
Expand All @@ -106,9 +106,9 @@ export function FeaturedCustomerCard({ story, active, emphasized }: FeaturedCust
preload='none'
src={story.media.src}
tabIndex={-1}
className='pointer-events-none absolute inset-0 size-full object-cover motion-reduce:hidden'
className='pointer-events-none absolute inset-0 size-full rounded-[inherit] object-cover motion-reduce:hidden'
/>
<div className='absolute inset-0 bg-[linear-gradient(135deg,rgba(0,0,0,0.68)_0%,rgba(0,0,0,0.3)_36%,rgba(0,0,0,0.08)_70%)]' />
<div className='pointer-events-none absolute inset-0 rounded-[inherit] bg-[linear-gradient(135deg,rgba(0,0,0,0.68)_0%,rgba(0,0,0,0.3)_36%,rgba(0,0,0,0.08)_70%)]' />
</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface CoreFeatureCardProps {
description: string
href: string
visual: ReactNode
interactiveVisual?: boolean
tone?: 'light' | 'mid' | 'dark'
}

Expand All @@ -33,35 +34,39 @@ const SCROLL_THROUGH = 'overscroll-x-auto [&_*]:overscroll-x-auto'

/**
* One homepage product module: a tall product-UI stage followed by a compact,
* independently quotable title and description. The whole module is a real
* route link, while the illustration remains decorative and non-interactive.
* independently quotable title and description. Interactive illustrations sit outside the route link;
* decorative illustrations remain part of the linked module.
*/
export function CoreFeatureCard({
title,
description,
href,
visual,
interactiveVisual = false,
tone = 'light',
}: CoreFeatureCardProps) {
const graphic = (
<div
aria-hidden={interactiveVisual ? undefined : true}
className={cn(
'relative aspect-[5/6] overflow-hidden border border-[var(--border)] transition-colors duration-300 group-hover:border-[var(--border)] motion-reduce:transition-none',
LANDING_STAGE_RADIUS,
SCROLL_THROUGH,
TONE_CLASSES[tone]
)}
>
<div className='absolute inset-0'>{visual}</div>
</div>
)
return (
<article className='min-w-0'>
{interactiveVisual ? graphic : null}
<Link
href={href}
rel={href.startsWith('https://') ? 'noopener noreferrer' : undefined}
className='group block outline-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[var(--border)] focus-visible:outline-offset-4'
>
<div
aria-hidden='true'
className={cn(
'relative aspect-[5/6] overflow-hidden border border-[var(--border)] transition-colors duration-300 group-hover:border-[var(--border)] motion-reduce:transition-none',
LANDING_STAGE_RADIUS,
SCROLL_THROUGH,
TONE_CLASSES[tone]
)}
>
<div className='absolute inset-0'>{visual}</div>
</div>

{interactiveVisual ? null : graphic}
<h3 className='mt-5 text-[20px] text-[var(--text-primary)] leading-[1.25] tracking-[-0.01em]'>
{title}
</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,39 @@ describe('FeaturesRail', () => {
expect(mount(true).scrollLeft).toBe(SET)
})

it('keeps interactive controls keyboard-accessible only in the home copy', () => {
let activations = 0
act(() =>
root?.render(
<FeaturesRail label='Interactive features'>
<div>
<button type='button' onClick={() => activations++}>
Open folder
</button>
<input aria-label='Search files' />
</div>
</FeaturesRail>
)
)

const homeControls = host?.querySelectorAll<HTMLElement>(
'[data-copy="home"] :is(button, input)'
)
expect(homeControls).toHaveLength(2)
for (const control of homeControls ?? []) expect(control.tabIndex).toBe(0)

const cloneControls = host?.querySelectorAll<HTMLElement>(
':is([data-copy="lead"], [data-copy="tail"]) :is(button, input)'
)
expect(cloneControls).toHaveLength(4)
for (const control of cloneControls ?? []) expect(control.tabIndex).toBe(-1)

const cloneButton = host?.querySelector<HTMLButtonElement>('[data-copy="tail"] button')
expect(cloneButton?.disabled).toBe(false)
act(() => cloneButton?.click())
expect(activations).toBe(1)
})

it('drags with the mouse, scrolling by the pointer delta and swallowing the click', () => {
const rail = mount()
const link = rail.querySelector<HTMLAnchorElement>('[data-copy="home"] a')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,10 @@ export function FeaturesRail({ label, children }: FeaturesRailProps) {
if (Math.abs(next - rail.scrollLeft) > FOLD_TOLERANCE) rail.scrollLeft = next
}

for (const link of rail.querySelectorAll<HTMLElement>(
'[data-copy="lead"] a, [data-copy="tail"] a'
for (const control of rail.querySelectorAll<HTMLElement>(
':is([data-copy="lead"], [data-copy="tail"]) :is(a, button, input, select, textarea, [tabindex])'
)) {
link.tabIndex = -1
control.tabIndex = -1
}
measure()
rail.scrollLeft = foldScrollLeft(rail.scrollLeft + setWidthRef.current, setWidthRef.current)
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/(landing)/components/features/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const CORE_FEATURES = [
href: '/files',
tone: 'mid',
visual: <FileLibraryGraphic />,
interactiveVisual: true,
},
{
title: 'Logs',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import { type ReactNode, useEffect, useRef, useState } from 'react'
import {
Button,
Check,
Expand Down Expand Up @@ -35,6 +35,8 @@ interface StageBlockCardProps {
selected?: boolean
/** Keeps the production selection toolbar visible in a noninteractive graphic. */
decorative?: boolean
/** Optional animated run glyph for a decorative, synchronized scene. */
decorativeRunIcon?: ReactNode
runStatus?: 'idle' | 'running' | 'complete'
onSelect?: (blockId: string) => void
onRunToggle?: (blockId: string) => void
Expand Down Expand Up @@ -201,6 +203,7 @@ export function StageBlockCard({
orientation = 'vertical',
selected = false,
decorative = false,
decorativeRunIcon,
runStatus = 'idle',
onSelect,
onRunToggle,
Expand Down Expand Up @@ -291,7 +294,9 @@ export function StageBlockCard({
onRunToggle?.(block.id)
}}
>
{running ? (
{decorative && decorativeRunIcon ? (
decorativeRunIcon
) : running ? (
<RunningActionIcon />
) : complete ? (
<Check className='size-[14px]' />
Expand All @@ -316,17 +321,19 @@ export function StageBlockCard({
variant='ghost'
size={null}
disabled={!decorative}
aria-label={`${label} unavailable in preview`}
className={cn(
'pointer-events-none',
getActionButtonClassName(label === 'Delete' ? 'last' : 'middle', selected)
aria-label={decorative ? label : `${label} unavailable in preview`}
className={getActionButtonClassName(
label === 'Delete' ? 'last' : 'middle',
selected
)}
>
<Icon className='size-[14px]' />
</Button>
</span>
</Tooltip.Trigger>
<Tooltip.Content side='top'>{label} is unavailable in preview</Tooltip.Content>
<Tooltip.Content side='top'>
{decorative ? label : `${label} unavailable in preview`}
</Tooltip.Content>
</Tooltip.Root>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@ describe('navbar customers menu', () => {
])
expect(CUSTOMERS_MENU.sections[0].items.map((item) => item.card?.tone)).toEqual([
'dark',
'light',
'dark',
])
expect(CUSTOMERS_MENU.sections[0].items[0].card?.background?.src).toMatch(
/^\/landing\/customers\/.+\.jpg$/
)
expect(CUSTOMERS_MENU.sections[0].items.map((item) => item.href)).toEqual([
'/customers/rivian',
'/customers/exp-realty',
])
for (const item of CUSTOMERS_MENU.sections[0].items) {
expect(item.card?.background?.src).toMatch(/^\/landing\/customers\/.+\.jpg$/)
expect(item.card?.imageSrc).toMatch(/^\/landing\/logos\/.+\.svg$/)
expect(item.card?.aspect).toBeGreaterThan(0)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ export const CUSTOMERS_MENU: NavMenu = {
imageAlt: 'eXp Realty',
aspect: 1.84,
height: 44,
tone: 'light',
tone: 'dark',
background: { src: '/landing/customers/exp-beach-house.jpg' },
},
preview: {
kind: 'resource',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ const CONTROL_QUIET = 'text-[var(--text-secondary)]'
*/
const CONTROL_COPY = 'text-[15px] leading-[1.45]'

/**
* The rail and controls share a group in `Landing`. Its positive gap provides
* breathing room above this inset rule; the padding balances the space below.
*/
const CONTROL_RULE = 'border-[var(--border)] border-t pt-16 max-sm:pt-12 max-lg:pt-14'

interface Control {
title: string
description: string
Expand Down Expand Up @@ -76,26 +70,22 @@ const CONTROLS: readonly Control[] = [
},
] as const

/**
* Operational controls as a heading-less white spec grid on the page measure,
* six items over two even rows. It reads as the plain-language index of the
* feature rail it follows, so it carries no title of its own: the section is
* named by `aria-label`, and the items stay `h3` under the rail's `h2` rather
* than becoming six competing section headings. Certifications and the
* governance intro live in `Security`, a separate beat further down the page.
*/
/** Six workspace capabilities grouped in a shared panel below the product rail. */
export function WorkspaceControls() {
return (
<section
id='controls'
aria-label='Workspace controls'
className={cn('flex w-full flex-col', LANDING_CONTENT_WIDTH, LANDING_GUTTER)}
>
<div className={cn(HOME_INSET, CONTROL_RULE)}>
<ul className='grid grid-cols-3 gap-x-16 gap-y-20 max-sm:grid-cols-1 max-sm:gap-y-12 max-lg:grid-cols-2 max-lg:gap-y-16'>
<div className={cn(HOME_INSET, 'pt-12 max-sm:pt-8')}>
<ul className='grid grid-cols-3 gap-px overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--border)] max-sm:grid-cols-1 max-lg:grid-cols-2'>
{CONTROLS.map(({ title, description, Mark }) => (
<li key={title} className='flex flex-col items-start gap-3'>
<Mark className={cn('size-[56px] max-sm:size-[48px]', CONTROL_QUIET)} />
<li
key={title}
className='flex flex-col items-start gap-3 bg-[var(--surface-2)] p-7 max-sm:p-6'
>
<Mark className={cn('size-[56px]', CONTROL_QUIET)} />
<div>
<h3 className={cn('text-[var(--text-primary)]', HOME_TYPE.body)}>{title}</h3>
<p className={cn('mt-1.5 max-w-[30ch] text-pretty', CONTROL_QUIET, CONTROL_COPY)}>
Expand Down
Loading
Loading