[Website] Extract HomeVisual into shared AppPreview section. (#20432)

This commit is contained in:
Abdullah.
2026-05-11 09:46:09 +05:00
committed by GitHub
parent 58dd5d3561
commit c81019965d
169 changed files with 679 additions and 956 deletions
@@ -1,42 +1,15 @@
import type {
HeroDashboardDataType,
HeroDashboardPageDefinition,
HeroKanbanPageDefinition,
HeroTablePageDefinition,
HeroVisualType,
} from '@/sections/Hero';
AppPreviewConfig,
DashboardData,
DashboardPageDefinition,
KanbanPageDefinition,
TablePageDefinition,
} from '@/sections/AppPreview';
import { SHARED_PEOPLE_AVATAR_URLS } from '@/content/site/asset-paths';
const PEOPLE_AVATAR_URLS = {
anonymousIndira: SHARED_PEOPLE_AVATAR_URLS.anonymousIndira,
anonymousLaura: SHARED_PEOPLE_AVATAR_URLS.anonymousLaura,
brianChesky: SHARED_PEOPLE_AVATAR_URLS.brianChesky,
benChestnut: SHARED_PEOPLE_AVATAR_URLS.benChestnut,
chrisWanstrath: SHARED_PEOPLE_AVATAR_URLS.chrisWanstrath,
craigFederighi: SHARED_PEOPLE_AVATAR_URLS.craigFederighi,
darioAmodei: SHARED_PEOPLE_AVATAR_URLS.darioAmodei,
dylanField: SHARED_PEOPLE_AVATAR_URLS.dylanField,
eddyCue: SHARED_PEOPLE_AVATAR_URLS.eddyCue,
ivanZhao: SHARED_PEOPLE_AVATAR_URLS.ivanZhao,
jeffWilliams: SHARED_PEOPLE_AVATAR_URLS.jeffWilliams,
joeGebbia: SHARED_PEOPLE_AVATAR_URLS.joeGebbia,
katherineAdams: SHARED_PEOPLE_AVATAR_URLS.katherineAdams,
patrickCollison: SHARED_PEOPLE_AVATAR_URLS.patrickCollison,
pingLi: SHARED_PEOPLE_AVATAR_URLS.pingLi,
peterReinhardt: SHARED_PEOPLE_AVATAR_URLS.peterReinhardt,
peterThiel: SHARED_PEOPLE_AVATAR_URLS.peterThiel,
philSchiller: SHARED_PEOPLE_AVATAR_URLS.philSchiller,
rayDamm: SHARED_PEOPLE_AVATAR_URLS.rayDamm,
reidHoffman: SHARED_PEOPLE_AVATAR_URLS.reidHoffman,
roelofBotha: SHARED_PEOPLE_AVATAR_URLS.roelofBotha,
ryanRoslansky: SHARED_PEOPLE_AVATAR_URLS.ryanRoslansky,
stewartButterfield: SHARED_PEOPLE_AVATAR_URLS.stewartButterfield,
sundarPichai: SHARED_PEOPLE_AVATAR_URLS.sundarPichai,
thomasDohmke: SHARED_PEOPLE_AVATAR_URLS.thomasDohmke,
timCook: SHARED_PEOPLE_AVATAR_URLS.timCook,
} as const;
const PEOPLE_AVATAR_URLS = SHARED_PEOPLE_AVATAR_URLS;
const SALES_DASHBOARD_DATA: HeroDashboardDataType = {
const SALES_DASHBOARD_DATA: DashboardData = {
metrics: [
{
id: 'new-subscriptions',
@@ -74,7 +47,7 @@ const SALES_DASHBOARD_DATA: HeroDashboardDataType = {
},
};
const SALES_DASHBOARD_PAGE: HeroDashboardPageDefinition = {
const SALES_DASHBOARD_PAGE: DashboardPageDefinition = {
type: 'dashboard',
header: {
title: 'Sales Dashboard',
@@ -82,7 +55,7 @@ const SALES_DASHBOARD_PAGE: HeroDashboardPageDefinition = {
dashboard: SALES_DASHBOARD_DATA,
};
const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
const OPPORTUNITY_KANBAN_PAGE: KanbanPageDefinition = {
type: 'kanban',
header: {
title: 'Best leads',
@@ -309,9 +282,9 @@ function createTablePage({
}: {
title: string;
count: number;
columns: HeroTablePageDefinition['columns'];
rows: HeroTablePageDefinition['rows'];
}): HeroTablePageDefinition {
columns: TablePageDefinition['columns'];
rows: TablePageDefinition['rows'];
}): TablePageDefinition {
return {
type: 'table',
header: {
@@ -323,7 +296,7 @@ function createTablePage({
};
}
export const HERO_DATA: { visual: HeroVisualType } = {
export const APP_PREVIEW_DATA: { visual: AppPreviewConfig } = {
visual: {
workspace: { icon: 'apple', name: 'Apple' },
tableWidth: 1700,
@@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { HELPED_CARDS } from '@/app/[locale]/(home)/helped.data';
import { HERO_DATA } from '@/app/[locale]/(home)/hero.data';
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
import { Problem, type ProblemPointType } from '@/sections/Problem';
import { HOME_TESTIMONIALS } from '@/app/[locale]/(home)/testimonials.data';
import { FEATURE_CARDS } from '@/app/[locale]/(home)/three-cards-feature.data';
@@ -235,7 +235,7 @@ export default async function HomePage({ params }: HomePageProps) {
/>
</Hero.Cta>
</HeroIntroGroup>
<Hero.HomeVisual visual={HERO_DATA.visual} />
<Hero.AppPreview visual={APP_PREVIEW_DATA.visual} />
</Hero.Root>
<TrustedBy.Root
@@ -336,7 +336,7 @@ export default async function HomePage({ params }: HomePageProps) {
eyebrow={i18n._(msg`They are the real sales`)}
testimonials={HOME_TESTIMONIALS}
>
<Testimonials.HomeVisual />
<Testimonials.HourglassVisual />
</Testimonials.Carousel>
</Testimonials.Root>
@@ -1,8 +0,0 @@
import type { ImageType } from '@/design-system/components/Image';
export const DEMO_DATA: { image: ImageType } = {
image: {
src: '/images/product/demo/kanban.webp',
alt: '',
},
};
@@ -3,7 +3,7 @@ import type { AppLocale } from 'twenty-shared/translations';
import { Faq, FAQ_QUESTIONS } from '@/sections/Faq';
import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy';
import { TalkToUsButton } from '@/sections/ContactCal';
import { DEMO_DATA } from '@/app/[locale]/product/demo.data';
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
import {
FEATURE_MASK,
FEATURE_TILES,
@@ -247,7 +247,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
variant="contained"
/>
</Demo.Cta>
<Demo.Screenshot image={DEMO_DATA.image} />
<Demo.Preview visual={APP_PREVIEW_DATA.visual} />
</Demo.Root>
<Faq.Root>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,19 +4,19 @@ import { scheduleVisualMount } from '@/lib/visual-runtime';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { useEffect } from 'react';
import type { HeroVisualType } from '@/sections/Hero/types';
import { DraggableAppWindow } from './DraggableAppWindow/DraggableAppWindow';
import { DraggableTerminal } from './DraggableTerminal/DraggableTerminal';
import { COLORS } from './Shared/utils/home-visual-theme';
import { HomeVisualNavbar } from './Shell/HomeVisualNavbar';
import { HomeVisualSidebar } from './Shell/HomeVisualSidebar';
import { HomeVisualViewbar } from './Shell/HomeVisualViewbar';
import { renderPageDefinition } from './Shell/HomeVisualPageRenderers';
import { preloadDeferredHomeVisualPages } from './Shell/preload-deferred-home-visual-pages';
import { useHomeVisualState } from './Shell/use-home-visual-state';
import type { AppPreviewConfig } from './types';
import { AppWindow } from './AppWindow/AppWindow';
import { Terminal } from './Terminal/Terminal';
import { COLORS } from './Shared/utils/app-preview-theme';
import { AppPreviewNavbar } from './Shell/AppPreviewNavbar';
import { AppPreviewSidebar } from './Shell/AppPreviewSidebar';
import { AppPreviewViewbar } from './Shell/AppPreviewViewbar';
import { renderPageDefinition } from './Shell/PageRenderers';
import { preloadDeferredPages } from './Shell/preload-deferred-pages';
import { useAppPreviewState } from './Shell/use-app-preview-state';
import { WindowOrderProvider } from './WindowOrder/WindowOrderProvider';
const StyledHomeVisual = styled.div`
const StyledAppPreview = styled.div`
isolation: isolate;
margin-top: ${theme.spacing(5)};
position: relative;
@@ -78,7 +78,12 @@ const IndexSurface = styled.div`
overflow: hidden;
`;
export function HomeVisual({ visual }: { visual: HeroVisualType }) {
type AppPreviewProps = {
showTerminal?: boolean;
visual: AppPreviewConfig;
};
export function AppPreview({ showTerminal = true, visual }: AppPreviewProps) {
const {
activeItem,
activeLabel,
@@ -92,7 +97,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
openFolderIds,
revealedObjectIds,
workspaceNav,
} = useHomeVisualState(visual);
} = useAppPreviewState(visual);
const activeHeader = activePage?.header;
const showViewBar =
activePage !== null &&
@@ -104,7 +109,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
() =>
scheduleVisualMount(
() => {
void preloadDeferredHomeVisualPages();
void preloadDeferredPages();
},
{ timeoutMs: 900 },
),
@@ -112,12 +117,12 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
);
return (
<StyledHomeVisual>
<StyledAppPreview>
<ShellScene>
<WindowOrderProvider>
<DraggableAppWindow>
<AppWindow>
<AppLayout>
<HomeVisualSidebar
<AppPreviewSidebar
favoritesNav={visual.favoritesNav}
highlightedItemId={highlightedItemId ?? undefined}
onSelectLabel={handleSelectLabel}
@@ -129,7 +134,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
/>
<RightPane>
<HomeVisualNavbar
<AppPreviewNavbar
activeItem={activeItem}
activeLabel={activeLabel}
navbarActions={activeHeader?.navbarActions}
@@ -138,7 +143,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
<IndexSurface>
{showViewBar ? (
<HomeVisualViewbar
<AppPreviewViewbar
actions={activeHeader?.actions ?? []}
count={activeHeader?.count}
pageType={activePage.type}
@@ -157,14 +162,16 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
</IndexSurface>
</RightPane>
</AppLayout>
</DraggableAppWindow>
<DraggableTerminal
onObjectCreated={handleObjectCreated}
onChatReset={handleChatReset}
onJumpToConversationEnd={handleJumpToConversationEnd}
/>
</AppWindow>
{showTerminal ? (
<Terminal
onObjectCreated={handleObjectCreated}
onChatReset={handleChatReset}
onJumpToConversationEnd={handleJumpToConversationEnd}
/>
) : null}
</WindowOrderProvider>
</ShellScene>
</StyledHomeVisual>
</StyledAppPreview>
);
}
@@ -9,7 +9,7 @@ import {
type ReactNode,
} from 'react';
import { theme } from '@/theme';
import { VISUAL_TOKENS } from '../Shared/utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../Shared/utils/app-preview-tokens';
import { useWindowPointerInteractions } from '../WindowInteraction/use-window-pointer-interactions';
import type {
WindowPosition as Position,
@@ -124,11 +124,11 @@ const ResizeCornerBottomRight = styled(ResizeCornerBase)`
right: -4px;
`;
type DraggableAppWindowProps = {
type AppWindowProps = {
children: ReactNode;
};
export const DraggableAppWindow = ({ children }: DraggableAppWindowProps) => {
export const AppWindow = ({ children }: AppWindowProps) => {
const shellRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<Position | null>(null);
@@ -2,7 +2,7 @@
import { styled } from '@linaria/react';
import type { PointerEvent as ReactPointerEvent } from 'react';
import { TerminalTrafficLights } from '../DraggableTerminal/TerminalTrafficLights/TerminalTrafficLights';
import { TerminalTrafficLights } from '../Terminal/TerminalTrafficLights/TerminalTrafficLights';
type MacWindowBarProps = {
title?: string;
@@ -1,17 +1,17 @@
import type {
HeroKanbanPageDefinition,
HeroPageDefinition,
HeroSidebarItem,
HeroTablePageDefinition,
HeroWorkflowPageDefinition,
} from '@/sections/Hero/types';
KanbanPageDefinition,
PageDefinition,
SidebarItemDef,
TablePageDefinition,
WorkflowPageDefinition,
} from '../types';
import type { HeroPageDefaults } from './hero-page-defaults';
import type { PageDefaults } from './page-defaults';
function normalizeTablePage(
page: HeroTablePageDefinition,
defaults: HeroPageDefaults,
): HeroTablePageDefinition {
page: TablePageDefinition,
defaults: PageDefaults,
): TablePageDefinition {
return {
...page,
header: {
@@ -25,9 +25,9 @@ function normalizeTablePage(
}
function normalizeKanbanPage(
page: HeroKanbanPageDefinition,
defaults: HeroPageDefaults,
): HeroKanbanPageDefinition {
page: KanbanPageDefinition,
defaults: PageDefaults,
): KanbanPageDefinition {
return {
...page,
header: {
@@ -42,9 +42,9 @@ function normalizeKanbanPage(
}
function normalizeExplicitPage(
page: HeroPageDefinition,
defaults: HeroPageDefaults,
): HeroPageDefinition {
page: PageDefinition,
defaults: PageDefaults,
): PageDefinition {
if (page.type === 'table') {
return normalizeTablePage(page, defaults);
}
@@ -67,8 +67,8 @@ function normalizeExplicitPage(
}
function normalizeWorkflowPage(
page: HeroWorkflowPageDefinition,
): HeroWorkflowPageDefinition {
page: WorkflowPageDefinition,
): WorkflowPageDefinition {
return {
...page,
header: {
@@ -79,10 +79,10 @@ function normalizeWorkflowPage(
};
}
export function normalizeHeroPage(
item: HeroSidebarItem,
defaults: HeroPageDefaults,
): HeroPageDefinition | null {
export function normalizePage(
item: SidebarItemDef,
defaults: PageDefaults,
): PageDefinition | null {
if (!item.page) {
return null;
}
@@ -1,35 +1,33 @@
import type { HeroNavbarActionType } from '@/sections/Hero/types';
import type { NavbarAction } from '../types';
type PinnedAction = HeroNavbarActionType;
const ROCKET_PINNED_ACTIONS: PinnedAction[] = [
const ROCKET_PINNED_ACTIONS: NavbarAction[] = [
{ icon: 'repeat', label: 'Fly again' },
{ icon: 'calendarPlus', label: 'Schedule launch' },
{ icon: 'playerPause', label: 'Retire' },
];
const LAUNCH_PINNED_ACTIONS: PinnedAction[] = [
const LAUNCH_PINNED_ACTIONS: NavbarAction[] = [
{ icon: 'calendarClock', label: 'Reschedule' },
{ icon: 'box', label: 'Add payload' },
{ icon: 'calendarEvent', label: 'Upcoming' },
];
const PAYLOAD_PINNED_ACTIONS: PinnedAction[] = [
const PAYLOAD_PINNED_ACTIONS: NavbarAction[] = [
{ icon: 'calendarPlus', label: 'Book slot' },
{ icon: 'flag', label: 'Set status' },
];
const COMPANIES_PINNED_ACTIONS: PinnedAction[] = [
const COMPANIES_PINNED_ACTIONS: NavbarAction[] = [
{ icon: 'flag', label: 'Set status' },
];
const LAUNCH_SITE_PINNED_ACTIONS: PinnedAction[] = [
const LAUNCH_SITE_PINNED_ACTIONS: NavbarAction[] = [
{ icon: 'flag', label: 'Set status' },
{ icon: 'calendarPlus', label: 'Book window' },
{ icon: 'rocket', label: 'Launches' },
];
export const OBJECT_PINNED_ACTIONS: Record<string, PinnedAction[]> = {
export const OBJECT_PINNED_ACTIONS: Record<string, NavbarAction[]> = {
rockets: ROCKET_PINNED_ACTIONS,
launches: LAUNCH_PINNED_ACTIONS,
payloads: PAYLOAD_PINNED_ACTIONS,
@@ -1,4 +1,4 @@
export type HeroPageDefaults = {
export type PageDefaults = {
defaultActions: string[];
defaultTableWidth?: number;
};
@@ -1,12 +1,9 @@
import type {
HeroSidebarItem,
HeroTablePageDefinition,
} from '@/sections/Hero/types';
import type { SidebarItemDef, TablePageDefinition } from '../types';
export const ROCKET_ITEM_ID = 'rockets';
export const ROCKET_ITEM_LABEL = 'Rockets';
const ROCKET_PAGE: HeroTablePageDefinition = {
const ROCKET_PAGE: TablePageDefinition = {
type: 'table',
header: {
title: 'All Rockets',
@@ -157,14 +154,14 @@ const ROCKET_PAGE: HeroTablePageDefinition = {
],
};
export const ROCKET_SIDEBAR_ITEM: HeroSidebarItem = {
export const ROCKET_SIDEBAR_ITEM: SidebarItemDef = {
id: ROCKET_ITEM_ID,
label: ROCKET_ITEM_LABEL,
icon: { kind: 'tabler', name: 'rocket', tone: 'violet' },
page: ROCKET_PAGE,
};
const LAUNCH_PAGE: HeroTablePageDefinition = {
const LAUNCH_PAGE: TablePageDefinition = {
type: 'table',
header: { title: 'Launches', count: 5 },
columns: [
@@ -291,7 +288,7 @@ const LAUNCH_PAGE: HeroTablePageDefinition = {
],
};
const PAYLOAD_PAGE: HeroTablePageDefinition = {
const PAYLOAD_PAGE: TablePageDefinition = {
type: 'table',
header: { title: 'Payloads', count: 5 },
columns: [
@@ -422,7 +419,7 @@ const PAYLOAD_PAGE: HeroTablePageDefinition = {
],
};
const LAUNCH_SITE_PAGE: HeroTablePageDefinition = {
const LAUNCH_SITE_PAGE: TablePageDefinition = {
type: 'table',
header: { title: 'Launch sites', count: 5 },
columns: [
@@ -511,21 +508,21 @@ const LAUNCH_SITE_PAGE: HeroTablePageDefinition = {
],
};
export const LAUNCH_SIDEBAR_ITEM: HeroSidebarItem = {
export const LAUNCH_SIDEBAR_ITEM: SidebarItemDef = {
id: 'launches',
label: 'Launches',
icon: { kind: 'tabler', name: 'calendarEvent', tone: 'violet' },
page: LAUNCH_PAGE,
};
export const PAYLOAD_SIDEBAR_ITEM: HeroSidebarItem = {
export const PAYLOAD_SIDEBAR_ITEM: SidebarItemDef = {
id: 'payloads',
label: 'Payloads',
icon: { kind: 'tabler', name: 'planet', tone: 'violet' },
page: PAYLOAD_PAGE,
};
export const LAUNCH_SITE_SIDEBAR_ITEM: HeroSidebarItem = {
export const LAUNCH_SITE_SIDEBAR_ITEM: SidebarItemDef = {
id: 'launch-sites',
label: 'Launch sites',
icon: { kind: 'tabler', name: 'mapPin', tone: 'violet' },
@@ -538,7 +535,7 @@ export const COMPANIES_ITEM_LABEL = 'Companies';
export const CRM_OBJECT_SEQUENCE: ReadonlyArray<{
id: string;
label: string;
sidebarItem: HeroSidebarItem;
sidebarItem: SidebarItemDef;
}> = [
{
id: ROCKET_ITEM_ID,
@@ -1,10 +1,7 @@
import type {
HeroDashboardChartImageType,
HeroDashboardPageDefinition,
} from '@/sections/Hero/types';
import type { DashboardChartImage, DashboardPageDefinition } from '../../types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { VISUAL_TOKENS } from '../../Shared/utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../../Shared/utils/app-preview-tokens';
const APP_FONT = VISUAL_TOKENS.font.family;
const CARD_BACKGROUND = VISUAL_TOKENS.background.secondary;
@@ -103,7 +100,7 @@ function DashboardChart({
chart,
className,
}: {
chart: HeroDashboardChartImageType;
chart: DashboardChartImage;
className?: string;
}) {
return (
@@ -121,7 +118,7 @@ function DashboardChart({
export function SalesDashboardPage({
page,
}: {
page: HeroDashboardPageDefinition;
page: DashboardPageDefinition;
}) {
return (
<DashboardGrid>
@@ -12,14 +12,10 @@ import {
IconUserCircle,
} from '@tabler/icons-react';
import type {
HeroCellEntity,
HeroCellPerson,
HeroKanbanCardType,
} from '@/sections/Hero/types';
import { HomeVisualFaviconLogo } from '../../Shared/components/HomeVisualFaviconLogo';
import { HomeVisualPersonAvatar } from '../../Shared/components/HomeVisualPersonAvatar';
import { Chip } from '../../Shared/components/HomeVisualChip';
import type { CellEntity, CellPerson, KanbanCard } from '../../types';
import { FaviconLogo } from '../../Shared/components/FaviconLogo';
import { PersonAvatar } from '../../Shared/components/PersonAvatar';
import { Chip } from '../../Shared/components/Chip';
import {
KANBAN_PAGE_COLORS,
KANBAN_PAGE_FONT,
@@ -142,25 +138,23 @@ const StarGlyph = styled.span`
width: 12px;
`;
function EntityChip({ entity }: { entity: HeroCellEntity }) {
function EntityChip({ entity }: { entity: CellEntity }) {
return (
<Chip
clickable={false}
label={entity.name}
leftComponent={
<HomeVisualFaviconLogo domain={entity.domain} label={entity.name} />
}
leftComponent={<FaviconLogo domain={entity.domain} label={entity.name} />}
maxWidth={152}
/>
);
}
function PersonChip({ person }: { person: HeroCellPerson }) {
function PersonChip({ person }: { person: CellPerson }) {
return (
<Chip
clickable={false}
label={person.name}
leftComponent={<HomeVisualPersonAvatar person={person} />}
leftComponent={<PersonAvatar person={person} />}
maxWidth={152}
/>
);
@@ -199,7 +193,7 @@ function Checkbox({ checked = false }: { checked?: boolean }) {
);
}
export function KanbanCard({ card }: { card: HeroKanbanCardType }) {
export function KanbanCard({ card }: { card: KanbanCard }) {
return (
<Card>
<CardHeader>
@@ -2,7 +2,7 @@ import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { IconPlus } from '@tabler/icons-react';
import type { HeroKanbanLaneType } from '@/sections/Hero/types';
import type { KanbanLane } from '../../types';
import { KanbanCard } from './KanbanCard';
import {
KANBAN_LANE_TONES,
@@ -80,7 +80,7 @@ export function KanbanLane({
isLast,
}: {
isLast: boolean;
lane: HeroKanbanLaneType;
lane: KanbanLane;
}) {
const tone = KANBAN_LANE_TONES[lane.tone] ?? KANBAN_LANE_TONES.gray;
@@ -1,7 +1,7 @@
'use client';
import { styled } from '@linaria/react';
import type { HeroKanbanPageDefinition } from '@/sections/Hero/types';
import type { KanbanPageDefinition } from '../../types';
import { KanbanLane } from './KanbanLane';
import { KANBAN_LANE_WIDTH } from './kanban-page-theme';
@@ -32,7 +32,7 @@ const BoardCanvas = styled.div<{ $laneCount: number }>`
width: 100%;
`;
export function KanbanPage({ page }: { page: HeroKanbanPageDefinition }) {
export function KanbanPage({ page }: { page: KanbanPageDefinition }) {
return (
<BoardShell
aria-label={`Interactive preview of the ${page.header.title} board`}
@@ -1,4 +1,4 @@
import { VISUAL_TOKENS } from '../../Shared/utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../../Shared/utils/app-preview-tokens';
export const KANBAN_PAGE_FONT = VISUAL_TOKENS.font.family;
export const KANBAN_PAGE_TABLER_STROKE = 1.6;
@@ -4,11 +4,11 @@ import { useHorizontalDragScroll } from '@/lib/dom/use-horizontal-drag-scroll';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { useState } from 'react';
import type { HeroTablePageDefinition } from '@/sections/Hero/types';
import type { TablePageDefinition } from '../../types';
import { IconChevronDown, IconPlus } from '@tabler/icons-react';
import { MiniIcon } from '../../Shared/components/MiniIcon';
import { TablePageCheckbox } from './TablePageCheckbox';
import { renderTableCellValue } from './TablePageCellValue';
import { ChevronDownMini } from './ChevronDownMini';
import { PlusMini } from './PlusMini';
import { renderTableHeaderIcon } from './render-table-header-icon';
import {
TABLE_PAGE_CELL_HORIZONTAL_PADDING,
@@ -68,10 +68,10 @@ const TableCanvas = styled.div<{ $width: number }>`
`;
const HeaderRow = styled.div`
animation: heroTableHeaderAppear 260ms ease-out both;
animation: tableHeaderAppear 260ms ease-out both;
display: flex;
@keyframes heroTableHeaderAppear {
@keyframes tableHeaderAppear {
from {
opacity: 0;
transform: translateY(-2px);
@@ -84,11 +84,11 @@ const HeaderRow = styled.div`
`;
const DataRow = styled.div<{ $rowIndex: number }>`
animation: heroTableRowAppear 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation: tableRowAppear 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: ${({ $rowIndex }) => `${120 + $rowIndex * 70}ms`};
display: flex;
@keyframes heroTableRowAppear {
@keyframes tableRowAppear {
from {
opacity: 0;
transform: translateY(6px);
@@ -214,7 +214,7 @@ export function TablePage({
page,
onNavigateToLabel,
}: {
page: HeroTablePageDefinition;
page: TablePageDefinition;
onNavigateToLabel?: (label: string) => void;
}) {
const {
@@ -272,7 +272,11 @@ export function TablePage({
{renderTableHeaderIcon(column.id)}
<HeaderLabel>{column.label}</HeaderLabel>
<EdgePlus aria-hidden="true">
<PlusMini color={COLORS.textTertiary} size={12} />
<MiniIcon
icon={IconPlus}
color={COLORS.textTertiary}
size={12}
/>
</EdgePlus>
</>
) : (
@@ -287,7 +291,11 @@ export function TablePage({
<EmptyFillCell $header $width={fillerWidth}>
{fillerWidth > 0 ? (
<HeaderFillContent>
<PlusMini color={COLORS.textTertiary} size={16} />
<MiniIcon
icon={IconPlus}
color={COLORS.textTertiary}
size={16}
/>
</HeaderFillContent>
) : null}
</EmptyFillCell>
@@ -343,7 +351,10 @@ export function TablePage({
>
<FooterFirstContent>
<MutedText>Calculate</MutedText>
<ChevronDownMini />
<MiniIcon
icon={IconChevronDown}
color={COLORS.textTertiary}
/>
</FooterFirstContent>
</TableCell>
) : null}
@@ -3,23 +3,21 @@ import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
import type {
HeroCellEntity,
HeroCellPerson,
HeroCellRelation,
HeroCellText,
HeroCellValue,
} from '@/sections/Hero/types';
import { getHomeVisualInitials } from '../../Shared/utils/get-home-visual-initials';
import { HomeVisualAvatar } from '../../Shared/components/HomeVisualAvatar';
import { HomeVisualFaviconLogo } from '../../Shared/components/HomeVisualFaviconLogo';
import { HomeVisualPersonAvatar } from '../../Shared/components/HomeVisualPersonAvatar';
CellEntity,
CellPerson,
CellRelation,
CellText,
CellValue,
} from '../../types';
import { getInitials } from '../../Shared/utils/get-initials';
import { PreviewAvatar } from '../../Shared/components/PreviewAvatar';
import { FaviconLogo } from '../../Shared/components/FaviconLogo';
import { PersonAvatar } from '../../Shared/components/PersonAvatar';
import { ChipVariant } from '../../Shared/utils/chip-variant';
import { Chip } from '../../Shared/components/HomeVisualChip';
import { VISUAL_TOKENS } from '../../Shared/utils/home-visual-tokens';
import { CheckMini } from './CheckMini';
import { CloseMini } from './CloseMini';
import { CopyMini } from './CopyMini';
import { PencilMini } from './PencilMini';
import { Chip } from '../../Shared/components/Chip';
import { VISUAL_TOKENS } from '../../Shared/utils/app-preview-tokens';
import { IconCheck, IconCopy, IconPencil, IconX } from '@tabler/icons-react';
import { MiniIcon } from '../../Shared/components/MiniIcon';
import { TablePageCheckbox } from './TablePageCheckbox';
import {
TABLE_PAGE_CELL_HORIZONTAL_PADDING,
@@ -151,7 +149,7 @@ function PersonTokenCell({
withCopyAction = true,
}: {
hovered?: boolean;
token: HeroCellPerson;
token: CellPerson;
withCopyAction?: boolean;
}) {
return (
@@ -159,12 +157,12 @@ function PersonTokenCell({
<CellChip
clickable={false}
label={token.name}
leftComponent={<HomeVisualPersonAvatar person={token} />}
leftComponent={<PersonAvatar person={token} />}
/>
<HoverActions $visible={hovered}>
{withCopyAction ? (
<MiniAction aria-hidden="true">
<CopyMini />
<MiniIcon icon={IconCopy} color={TABLE_PAGE_COLORS.textSecondary} />
</MiniAction>
) : null}
</HoverActions>
@@ -177,7 +175,7 @@ function EntityCellComponent({
hovered,
isFirstColumn,
}: {
cell: HeroCellEntity;
cell: CellEntity;
hovered: boolean;
isFirstColumn: boolean;
}) {
@@ -188,14 +186,15 @@ function EntityCellComponent({
<CellChip
clickable={false}
label={cell.name}
leftComponent={
<HomeVisualFaviconLogo domain={cell.domain} label={cell.name} />
}
leftComponent={<FaviconLogo domain={cell.domain} label={cell.name} />}
variant={ChipVariant.Highlighted}
/>
<HoverActions $visible={hovered}>
<MiniAction aria-hidden="true">
<PencilMini />
<MiniIcon
icon={IconPencil}
color={TABLE_PAGE_COLORS.textSecondary}
/>
</MiniAction>
</HoverActions>
</EntityCellLayout>
@@ -206,9 +205,7 @@ function EntityCellComponent({
<CellChip
clickable={false}
label={cell.name}
leftComponent={
<HomeVisualFaviconLogo domain={cell.domain} label={cell.name} />
}
leftComponent={<FaviconLogo domain={cell.domain} label={cell.name} />}
/>
);
}
@@ -217,7 +214,7 @@ function RelationCellComponent({
cell,
hovered,
}: {
cell: HeroCellRelation;
cell: CellRelation;
hovered: boolean;
}) {
return (
@@ -229,16 +226,16 @@ function RelationCellComponent({
clickable={false}
label={item.name}
leftComponent={
<HomeVisualAvatar tone={item.tone}>
{item.shortLabel ?? getHomeVisualInitials(item.name)}
</HomeVisualAvatar>
<PreviewAvatar tone={item.tone}>
{item.shortLabel ?? getInitials(item.name)}
</PreviewAvatar>
}
/>
))}
</MultiChipStack>
<HoverActions $visible={hovered}>
<MiniAction aria-hidden="true">
<CopyMini />
<MiniIcon icon={IconCopy} color={TABLE_PAGE_COLORS.textSecondary} />
</MiniAction>
</HoverActions>
</CellHoverAnchor>
@@ -250,7 +247,7 @@ function TextCellComponent({
isFirstColumn,
onNavigateToLabel,
}: {
cell: HeroCellText;
cell: CellText;
isFirstColumn: boolean;
onNavigateToLabel?: (label: string) => void;
}) {
@@ -269,7 +266,7 @@ function TextCellComponent({
clickable={handleNavigate !== undefined}
label={cell.value}
leftComponent={
<HomeVisualAvatar tone={cell.tone}>{cell.shortLabel}</HomeVisualAvatar>
<PreviewAvatar tone={cell.tone}>{cell.shortLabel}</PreviewAvatar>
}
onClick={handleNavigate}
/>
@@ -283,7 +280,7 @@ export function renderTableCellValue({
isFirstColumn,
onNavigateToLabel,
}: {
cell: HeroCellValue;
cell: CellValue;
columnId: string;
hovered: boolean;
isFirstColumn: boolean;
@@ -315,7 +312,15 @@ export function renderTableCellValue({
case 'boolean':
return (
<BooleanRow>
{cell.value ? <CheckMini size={11} /> : <CloseMini size={11} />}
{cell.value ? (
<MiniIcon
icon={IconCheck}
color={TABLE_PAGE_COLORS.text}
size={11}
/>
) : (
<MiniIcon icon={IconX} color={TABLE_PAGE_COLORS.text} size={11} />
)}
<InlineText>{cell.value ? 'True' : 'False'}</InlineText>
</BooleanRow>
);
@@ -1,4 +1,4 @@
import { VISUAL_TOKENS } from '../../Shared/utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../../Shared/utils/app-preview-tokens';
export const TABLE_PAGE_FONT = VISUAL_TOKENS.font.family;
export const TABLE_PAGE_CELL_HORIZONTAL_PADDING = 8;
@@ -32,7 +32,7 @@ export function WorkflowEdges({
>
<defs>
<marker
id="hero-workflow-arrow"
id="workflow-arrow"
markerHeight="6"
markerWidth="6"
orient="auto"
@@ -54,7 +54,7 @@ export function WorkflowEdges({
key={`${edge.from}-${edge.to}`}
d={getWorkflowEdgePath({ edge, nodes })}
fill="none"
markerEnd="url(#hero-workflow-arrow)"
markerEnd="url(#workflow-arrow)"
stroke={WORKFLOW_PAGE_COLORS.arrowStroke}
strokeLinecap="round"
strokeLinejoin="round"
@@ -1,6 +1,6 @@
'use client';
import type { HeroWorkflowPageDefinition } from '@/sections/Hero/types';
import type { WorkflowPageDefinition } from '../../types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
@@ -94,7 +94,7 @@ const ActiveBadgeLabel = styled.span`
padding: 0 8px;
`;
export function WorkflowPage({ page }: { page: HeroWorkflowPageDefinition }) {
export function WorkflowPage({ page }: { page: WorkflowPageDefinition }) {
return (
<PageShell>
<CanvasViewportShell>
@@ -1,4 +1,4 @@
import { VISUAL_TOKENS } from '../../Shared/utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../../Shared/utils/app-preview-tokens';
export const WORKFLOW_PAGE_FONT = VISUAL_TOKENS.font.family;
export const WORKFLOW_PAGE_TABLER_STROKE = 1.6;
@@ -3,7 +3,7 @@ import { type KeyboardEvent, type MouseEvent, type PointerEvent } from 'react';
import type { ChipProps } from '../utils/chip-props';
import { ChipVariant } from '../utils/chip-variant';
import { VISUAL_TOKENS } from '../utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../utils/app-preview-tokens';
const StyledContainer = styled.div<
Pick<ChipProps, 'clickable' | 'isBold' | 'maxWidth' | 'variant'>
@@ -3,8 +3,8 @@ import { createBoundedFailureCache } from '@/lib/visual-runtime';
import { styled } from '@linaria/react';
import { useState } from 'react';
import { getHomeVisualInitials } from '../utils/get-home-visual-initials';
import { APP_FONT } from '../utils/home-visual-theme';
import { getInitials } from '../utils/get-initials';
import { APP_FONT } from '../utils/app-preview-theme';
const failedFaviconUrls = createBoundedFailureCache(256);
@@ -41,9 +41,7 @@ function sanitizeURL(link: string | null | undefined) {
: '';
}
function getHomeVisualLogoUrlFromDomainName(
domainName?: string,
): string | undefined {
function getLogoUrlFromDomainName(domainName?: string): string | undefined {
const sharedLogoUrl = getSharedCompanyLogoUrlFromDomainName(domainName);
if (sharedLogoUrl) {
@@ -57,7 +55,7 @@ function getHomeVisualLogoUrlFromDomainName(
: undefined;
}
export function HomeVisualFaviconLogo({
export function FaviconLogo({
domain,
label,
size = 14,
@@ -68,7 +66,7 @@ export function HomeVisualFaviconLogo({
size?: number;
src?: string;
}) {
const faviconUrl = src ?? getHomeVisualLogoUrlFromDomainName(domain);
const faviconUrl = src ?? getLogoUrlFromDomainName(domain);
const [localFailedUrl, setLocalFailedUrl] = useState<string | null>(null);
const showFavicon =
faviconUrl !== undefined &&
@@ -90,7 +88,7 @@ export function HomeVisualFaviconLogo({
);
}
const initials = label ? getHomeVisualInitials(label) : '?';
const initials = label ? getInitials(label) : '?';
return (
<FaviconFallbackFrame $size={size}>
@@ -0,0 +1,22 @@
import type { ComponentType } from 'react';
type MiniIconProps = {
color?: string;
icon: ComponentType<{
'aria-hidden'?: boolean;
color?: string;
size?: number;
stroke?: number;
}>;
size?: number;
stroke?: number;
};
export function MiniIcon({
color,
icon: Icon,
size = 14,
stroke = 1.6,
}: MiniIconProps) {
return <Icon aria-hidden color={color} size={size} stroke={stroke} />;
}
@@ -1,5 +1,5 @@
import { styled } from '@linaria/react';
import { HomeVisualLoader } from './HomeVisualLoader';
import { PreviewLoader } from './PreviewLoader';
const StyledPagePreviewLoader = styled.div`
align-items: center;
@@ -19,7 +19,7 @@ type PagePreviewLoaderProps = {
export function PagePreviewLoader({ ariaLabel }: PagePreviewLoaderProps) {
return (
<StyledPagePreviewLoader aria-label={ariaLabel}>
<HomeVisualLoader />
<PreviewLoader />
</StyledPagePreviewLoader>
);
}
@@ -2,15 +2,15 @@ import { createBoundedFailureCache } from '@/lib/visual-runtime';
import { styled } from '@linaria/react';
import { useState } from 'react';
import type { HeroCellPerson } from '@/sections/Hero/types';
import type { CellPerson } from '../../types';
import { getHomeVisualInitials } from '../utils/get-home-visual-initials';
import { HomeVisualAvatar } from './HomeVisualAvatar';
import { getInitials } from '../utils/get-initials';
import { PreviewAvatar } from './PreviewAvatar';
const failedAvatarUrls = createBoundedFailureCache(256);
type HomeVisualPersonIdentity = Pick<
HeroCellPerson,
type PersonIdentity = Pick<
CellPerson,
'avatarUrl' | 'kind' | 'name' | 'shortLabel' | 'tone'
>;
@@ -21,11 +21,11 @@ const AvatarImage = styled.img`
width: 100%;
`;
export function HomeVisualPersonAvatar({
export function PersonAvatar({
person,
size = 14,
}: {
person: HomeVisualPersonIdentity;
person: PersonIdentity;
size?: number;
}) {
const [localFailedUrl, setLocalFailedUrl] = useState<string | null>(null);
@@ -39,7 +39,7 @@ export function HomeVisualPersonAvatar({
localFailedUrl !== person.avatarUrl;
return (
<HomeVisualAvatar size={size} square={square} tone={person.tone}>
<PreviewAvatar size={size} square={square} tone={person.tone}>
{showAvatar ? (
<AvatarImage
alt=""
@@ -52,8 +52,8 @@ export function HomeVisualPersonAvatar({
}}
/>
) : (
(person.shortLabel ?? getHomeVisualInitials(person.name))
(person.shortLabel ?? getInitials(person.name))
)}
</HomeVisualAvatar>
</PreviewAvatar>
);
}
@@ -2,12 +2,9 @@ import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
import { APP_FONT } from '../utils/home-visual-theme';
import { APP_FONT } from '../utils/app-preview-theme';
const HOME_VISUAL_PERSON_TONES: Record<
string,
{ background: string; color: string }
> = {
const PERSON_TONES: Record<string, { background: string; color: string }> = {
amber: { background: '#f6e6d7', color: '#7a4f2a' },
blue: { background: '#dbeafe', color: '#1d4ed8' },
gray: { background: '#e5e7eb', color: '#4b5563' },
@@ -41,7 +38,7 @@ const AvatarFrame = styled.div<{
width: ${({ $size }) => `${$size}px`};
`;
export function HomeVisualAvatar({
export function PreviewAvatar({
children,
size = 14,
square = false,
@@ -52,8 +49,7 @@ export function HomeVisualAvatar({
square?: boolean;
tone?: string;
}) {
const resolvedTone =
HOME_VISUAL_PERSON_TONES[tone] ?? HOME_VISUAL_PERSON_TONES.gray;
const resolvedTone = PERSON_TONES[tone] ?? PERSON_TONES.gray;
return (
<AvatarFrame
@@ -20,14 +20,14 @@ import {
} from '@tabler/icons-react';
import type { ReactNode } from 'react';
import type { HeroSidebarIcon } from '@/sections/Hero/types';
import { HomeVisualFaviconLogo } from './HomeVisualFaviconLogo';
import type { SidebarIcon } from '../../types';
import { FaviconLogo } from './FaviconLogo';
import {
APP_FONT,
COLORS,
NAVIGATION_TABLER_STROKE,
TABLER_STROKE,
} from '../utils/home-visual-theme';
} from '../utils/app-preview-theme';
const SIDEBAR_TONES: Record<
string,
@@ -73,7 +73,7 @@ const SidebarIconSurface = styled.div<{
align-items: center;
animation: ${({ $pulse }) =>
$pulse
? 'heroObjectAppearIcon 1400ms cubic-bezier(0.34, 1.7, 0.64, 1) both'
? 'objectAppearIcon 1400ms cubic-bezier(0.34, 1.7, 0.64, 1) both'
: 'none'};
background: ${({ $background }) => $background};
border: 1px solid ${({ $border }) => $border};
@@ -86,7 +86,7 @@ const SidebarIconSurface = styled.div<{
position: relative;
width: 16px;
@keyframes heroObjectAppearIcon {
@keyframes objectAppearIcon {
0% {
transform: scale(0.35) rotate(-18deg);
}
@@ -158,8 +158,8 @@ function LinkOverlay() {
);
}
export function renderHomeVisualIcon(
icon: HeroSidebarIcon,
export function renderPreviewIcon(
icon: SidebarIcon,
pulse: boolean = false,
): ReactNode {
if (icon.kind === 'brand') {
@@ -170,7 +170,7 @@ export function renderHomeVisualIcon(
$color={COLORS.textSecondary}
$pulse={pulse}
>
<HomeVisualFaviconLogo
<FaviconLogo
domain={
icon.domain ??
(icon.brand === 'claude'
@@ -1,6 +1,6 @@
import { styled } from '@linaria/react';
import { motion } from 'framer-motion';
import { VISUAL_TOKENS } from '../utils/home-visual-tokens';
import { VISUAL_TOKENS } from '../utils/app-preview-tokens';
const DEFAULT_LOADER_COLOR = `var(--tw-button-color, ${VISUAL_TOKENS.font.color.tertiary})`;
@@ -24,11 +24,11 @@ const StyledLoader = styled(motion.div)`
width: 8px;
`;
type HomeVisualLoaderProps = {
type PreviewLoaderProps = {
color?: string;
};
export function HomeVisualLoader({ color }: HomeVisualLoaderProps) {
export function PreviewLoader({ color }: PreviewLoaderProps) {
return (
<StyledLoaderContainer style={color ? { borderColor: color } : undefined}>
<StyledLoader
@@ -1,4 +1,4 @@
import { VISUAL_TOKENS } from './home-visual-tokens';
import { VISUAL_TOKENS } from './app-preview-tokens';
export const APP_FONT = VISUAL_TOKENS.font.family;
@@ -0,0 +1,9 @@
export const ChipVariant = {
Highlighted: 'highlighted',
Regular: 'regular',
Transparent: 'transparent',
Rounded: 'rounded',
Static: 'static',
} as const;
export type ChipVariant = (typeof ChipVariant)[keyof typeof ChipVariant];
@@ -1,4 +1,4 @@
export function getHomeVisualInitials(value: string) {
export function getInitials(value: string) {
return value
.split(' ')
.filter(Boolean)
@@ -1,4 +1,4 @@
import type { HeroSidebarIcon } from '@/sections/Hero/types';
import type { SidebarIcon } from '../../types';
const SIDEBAR_TONES: Record<string, { color: string }> = {
amber: { color: '#35290F' },
@@ -33,7 +33,7 @@ const SIDEBAR_TONE_RGB: Record<string, string> = Object.fromEntries(
]),
);
export function getSidebarIconToneRgb(icon: HeroSidebarIcon): string {
export function getSidebarIconToneRgb(icon: SidebarIcon): string {
const iconTone =
'tone' in icon && typeof icon.tone === 'string' ? icon.tone : 'gray';
@@ -19,18 +19,15 @@ import {
} from '@tabler/icons-react';
import type { ReactNode } from 'react';
import type {
HeroNavbarActionType,
HeroSidebarItem,
} from '@/sections/Hero/types';
import type { NavbarAction, SidebarItemDef } from '../types';
import { OBJECT_PINNED_ACTIONS } from '../Data/object-pinned-actions';
import { renderHomeVisualIcon } from '../Shared/components/HomeVisualIcon';
import { renderPreviewIcon } from '../Shared/components/PreviewIcon';
import {
APP_FONT,
COLORS,
NAVBAR_ACTION_TABLER_STROKE,
} from '../Shared/utils/home-visual-theme';
import { VISUAL_TOKENS } from '../Shared/utils/home-visual-tokens';
} from '../Shared/utils/app-preview-theme';
import { VISUAL_TOKENS } from '../Shared/utils/app-preview-tokens';
const NAVBAR_ACTION_ICON_MAP: Record<string, typeof IconPlus> = {
box: IconBox,
@@ -202,9 +199,7 @@ const PinnedActionButton = styled(NavbarActionButton)<{
}
`;
function getNavbarActionToneColor(
tone: HeroNavbarActionType['labelTone'],
): string {
function getNavbarActionToneColor(tone: NavbarAction['labelTone']): string {
if (tone === 'primary') {
return VISUAL_TOKENS.font.color.primary;
}
@@ -216,10 +211,7 @@ function getNavbarActionToneColor(
return VISUAL_TOKENS.font.color.secondary;
}
function renderNavbarAction(
action: HeroNavbarActionType,
index: number,
): ReactNode {
function renderNavbarAction(action: NavbarAction, index: number): ReactNode {
const ActionIcon = NAVBAR_ACTION_ICON_MAP[action.icon];
const isIconOnly =
action.variant === 'icon' || (!action.label && !action.trailingLabel);
@@ -267,7 +259,7 @@ function renderNavbarAction(
}
function renderPinnedAction(
action: HeroNavbarActionType,
action: NavbarAction,
index: number,
activeItemId: string | undefined,
) {
@@ -291,7 +283,7 @@ function renderPinnedAction(
}
function renderDefaultActions(
activeItem: HeroSidebarItem | undefined,
activeItem: SidebarItemDef | undefined,
revealedObjectIds: string[],
) {
const pinnedActions =
@@ -335,24 +327,24 @@ function renderDefaultActions(
);
}
type HomeVisualNavbarProps = {
activeItem?: HeroSidebarItem;
type AppPreviewNavbarProps = {
activeItem?: SidebarItemDef;
activeLabel: string;
navbarActions?: HeroNavbarActionType[];
navbarActions?: NavbarAction[];
revealedObjectIds: string[];
};
export function HomeVisualNavbar({
export function AppPreviewNavbar({
activeItem,
activeLabel,
navbarActions,
revealedObjectIds,
}: HomeVisualNavbarProps) {
}: AppPreviewNavbarProps) {
return (
<NavbarBar>
<Breadcrumb>
<BreadcrumbTag>
{activeItem ? renderHomeVisualIcon(activeItem.icon) : null}
{activeItem ? renderPreviewIcon(activeItem.icon) : null}
<CrumbLabel>{activeLabel}</CrumbLabel>
</BreadcrumbTag>
</Breadcrumb>
@@ -4,12 +4,12 @@ import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
import type { HeroSidebarEntry, HeroSidebarItem } from '@/sections/Hero/types';
import { HomeVisualSidebarControls } from './HomeVisualSidebarControls';
import { HomeVisualSidebarHeader } from './HomeVisualSidebarHeader';
import { HomeVisualSidebarItem } from './HomeVisualSidebarItem';
import type { SidebarEntry, SidebarItemDef } from '../types';
import { SidebarControls } from './SidebarControls';
import { SidebarHeader } from './SidebarHeader';
import { SidebarItem } from './SidebarItem';
import { isFolder } from './is-folder';
import { APP_FONT, COLORS } from '../Shared/utils/home-visual-theme';
import { APP_FONT, COLORS } from '../Shared/utils/app-preview-theme';
const SidebarPanel = styled.aside`
background: transparent;
@@ -63,18 +63,18 @@ const SidebarSectionLabel = styled.span<{ $workspace?: boolean }>`
}
`;
type HomeVisualSidebarProps = {
favoritesNav?: HeroSidebarItem[];
type AppPreviewSidebarProps = {
favoritesNav?: SidebarItemDef[];
highlightedItemId?: string;
onSelectLabel: (label: string) => void;
onToggleFolder: (folderId: string) => void;
openFolderIds: string[];
selectedLabel: string;
workspaceName: string;
workspaceNav: HeroSidebarEntry[];
workspaceNav: SidebarEntry[];
};
export function HomeVisualSidebar({
export function AppPreviewSidebar({
favoritesNav,
highlightedItemId,
onSelectLabel,
@@ -83,11 +83,11 @@ export function HomeVisualSidebar({
selectedLabel,
workspaceName,
workspaceNav,
}: HomeVisualSidebarProps) {
const renderSidebarEntry = (entry: HeroSidebarEntry): ReactNode => {
}: AppPreviewSidebarProps) {
const renderSidebarEntry = (entry: SidebarEntry): ReactNode => {
if (isFolder(entry)) {
return (
<HomeVisualSidebarItem
<SidebarItem
collapsible
expanded={openFolderIds.includes(entry.id)}
highlightedItemId={highlightedItemId}
@@ -107,7 +107,7 @@ export function HomeVisualSidebar({
}
return (
<HomeVisualSidebarItem
<SidebarItem
highlightedItemId={highlightedItemId}
key={entry.id}
item={entry}
@@ -119,16 +119,16 @@ export function HomeVisualSidebar({
return (
<SidebarPanel>
<HomeVisualSidebarHeader workspaceName={workspaceName} />
<SidebarHeader workspaceName={workspaceName} />
<HomeVisualSidebarControls />
<SidebarControls />
<SidebarScroll>
{favoritesNav && favoritesNav.length > 0 ? (
<SidebarSection>
<SidebarSectionLabel>Favorites</SidebarSectionLabel>
{favoritesNav.map((item) => (
<HomeVisualSidebarItem
<SidebarItem
highlightedItemId={highlightedItemId}
key={item.id}
item={item}
@@ -8,12 +8,9 @@ import {
IconList,
} from '@tabler/icons-react';
import type { HeroPageType } from '@/sections/Hero/types';
import {
APP_FONT,
COLORS,
TABLER_STROKE,
} from '../Shared/utils/home-visual-theme';
import type { PageType } from '../types';
import { MiniIcon } from '../Shared/components/MiniIcon';
import { APP_FONT, COLORS } from '../Shared/utils/app-preview-theme';
const ViewbarBar = styled.div`
align-items: center;
@@ -90,60 +87,21 @@ const ViewAction = styled.span`
white-space: nowrap;
`;
type MiniIconProps = {
color?: string;
size?: number;
};
function ChevronDownMini({
color = COLORS.textTertiary,
size = 14,
}: MiniIconProps) {
return (
<IconChevronDown
aria-hidden
color={color}
size={size}
stroke={TABLER_STROKE}
/>
);
}
function ListMini({ color = COLORS.textSecondary, size = 16 }: MiniIconProps) {
return (
<IconList aria-hidden color={color} size={size} stroke={TABLER_STROKE} />
);
}
function KanbanMini({
color = COLORS.textSecondary,
size = 16,
}: MiniIconProps) {
return (
<IconLayoutKanban
aria-hidden
color={color}
size={size}
stroke={TABLER_STROKE}
/>
);
}
type HomeVisualViewbarProps = {
type AppPreviewViewbarProps = {
actions: string[];
count?: number;
pageType: HeroPageType;
pageType: PageType;
showListIcon: boolean;
title: string;
};
export function HomeVisualViewbar({
export function AppPreviewViewbar({
actions,
count,
pageType,
showListIcon,
title,
}: HomeVisualViewbarProps) {
}: AppPreviewViewbarProps) {
const showPageCount = count !== undefined;
return (
@@ -151,13 +109,25 @@ export function HomeVisualViewbar({
<ViewSwitcher aria-hidden="true">
{showListIcon ? (
<>
{pageType === 'kanban' ? <KanbanMini /> : <ListMini />}
{pageType === 'kanban' ? (
<MiniIcon
icon={IconLayoutKanban}
color={COLORS.textSecondary}
size={16}
/>
) : (
<MiniIcon
icon={IconList}
color={COLORS.textSecondary}
size={16}
/>
)}
<ViewName>{title}</ViewName>
{showPageCount ? (
<>
<TinyDot />
<ViewCount>{count}</ViewCount>
<ChevronDownMini color={COLORS.textLight} />
<MiniIcon icon={IconChevronDown} color={COLORS.textLight} />
</>
) : null}
</>
@@ -5,13 +5,13 @@ import dynamic from 'next/dynamic';
import type { ReactNode } from 'react';
import type {
HeroDashboardPageDefinition,
HeroKanbanPageDefinition,
HeroPageDefinition,
HeroPageType,
HeroTablePageDefinition,
HeroWorkflowPageDefinition,
} from '@/sections/Hero/types';
DashboardPageDefinition,
KanbanPageDefinition,
PageDefinition,
PageType,
TablePageDefinition,
WorkflowPageDefinition,
} from '../types';
import { KanbanPage } from '../Pages/Kanban/KanbanPage';
import { TablePage } from '../Pages/Table/TablePage';
import { PagePreviewLoader } from '../Shared/components/PagePreviewLoader';
@@ -59,24 +59,22 @@ const WorkflowPage = dynamic(
);
const PAGE_RENDERERS = {
table: (page: HeroTablePageDefinition) => <TablePage page={page} />,
kanban: (page: HeroKanbanPageDefinition) => <KanbanPage page={page} />,
dashboard: (page: HeroDashboardPageDefinition) => (
table: (page: TablePageDefinition) => <TablePage page={page} />,
kanban: (page: KanbanPageDefinition) => <KanbanPage page={page} />,
dashboard: (page: DashboardPageDefinition) => (
<DashboardViewport
aria-label={`Interactive preview of the ${page.header.title.toLowerCase()}`}
>
<SalesDashboardPage page={page} />
</DashboardViewport>
),
workflow: (page: HeroWorkflowPageDefinition) => <WorkflowPage page={page} />,
workflow: (page: WorkflowPageDefinition) => <WorkflowPage page={page} />,
} satisfies {
[K in HeroPageType]: (
page: Extract<HeroPageDefinition, { type: K }>,
) => ReactNode;
[K in PageType]: (page: Extract<PageDefinition, { type: K }>) => ReactNode;
};
export function renderPageDefinition(
page: HeroPageDefinition,
page: PageDefinition,
onNavigateToLabel?: (label: string) => void,
pageKey?: string,
) {
@@ -12,9 +12,9 @@ import {
APP_FONT,
COLORS,
TABLER_STROKE,
} from '../Shared/utils/home-visual-theme';
} from '../Shared/utils/app-preview-theme';
const SidebarControls = styled.div`
const SidebarControlsRoot = styled.div`
align-items: center;
display: grid;
gap: 8px;
@@ -134,9 +134,9 @@ function MessageCirclePlusMini({
);
}
export function HomeVisualSidebarControls() {
export function SidebarControls() {
return (
<SidebarControls>
<SidebarControlsRoot>
<SegmentedRail aria-hidden="true">
<Segment $selected>
<HomeMini />
@@ -149,6 +149,6 @@ export function HomeVisualSidebarControls() {
<MessageCirclePlusMini />
<NewChatLabel>New chat</NewChatLabel>
</NewChat>
</SidebarControls>
</SidebarControlsRoot>
);
}
@@ -8,11 +8,8 @@ import {
IconSearch,
} from '@tabler/icons-react';
import {
APP_FONT,
COLORS,
TABLER_STROKE,
} from '../Shared/utils/home-visual-theme';
import { MiniIcon } from '../Shared/components/MiniIcon';
import { APP_FONT, COLORS } from '../Shared/utils/app-preview-theme';
const APPLE_WORKSPACE_LOGO_SRC = '/images/home/hero/apple-rainbow-logo.svg';
@@ -108,52 +105,11 @@ const SidebarIconButton = styled.div`
width: 24px;
`;
type MiniIconProps = {
color?: string;
size?: number;
};
function ChevronDownMini({
color = COLORS.textTertiary,
size = 14,
}: MiniIconProps) {
return (
<IconChevronDown
aria-hidden
color={color}
size={size}
stroke={TABLER_STROKE}
/>
);
}
function SearchMini({ color = COLORS.textTertiary, size = 16 }: MiniIconProps) {
return (
<IconSearch aria-hidden color={color} size={size} stroke={TABLER_STROKE} />
);
}
function CollapseSidebarMini({
color = COLORS.textTertiary,
size = 16,
}: MiniIconProps) {
return (
<IconLayoutSidebarLeftCollapse
aria-hidden
color={color}
size={size}
stroke={TABLER_STROKE}
/>
);
}
type HomeVisualSidebarHeaderProps = {
type SidebarHeaderProps = {
workspaceName: string;
};
export function HomeVisualSidebarHeader({
workspaceName,
}: HomeVisualSidebarHeaderProps) {
export function SidebarHeader({ workspaceName }: SidebarHeaderProps) {
return (
<SidebarTopBar>
<WorkspaceMenu>
@@ -165,14 +121,18 @@ export function HomeVisualSidebarHeader({
/>
</WorkspaceIcon>
<WorkspaceLabel>{workspaceName}</WorkspaceLabel>
<ChevronDownMini color={COLORS.textLight} size={12} />
<MiniIcon icon={IconChevronDown} color={COLORS.textLight} size={12} />
</WorkspaceMenu>
<SidebarTopActions>
<SidebarIconButton aria-hidden="true">
<SearchMini />
<MiniIcon icon={IconSearch} color={COLORS.textTertiary} size={16} />
</SidebarIconButton>
<SidebarIconButton aria-hidden="true">
<CollapseSidebarMini />
<MiniIcon
icon={IconLayoutSidebarLeftCollapse}
color={COLORS.textTertiary}
size={16}
/>
</SidebarIconButton>
</SidebarTopActions>
</SidebarTopBar>
@@ -4,15 +4,12 @@ import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { IconChevronDown } from '@tabler/icons-react';
import type { HeroSidebarItem } from '@/sections/Hero/types';
import type { SidebarItemDef } from '../types';
import { getSidebarIconToneRgb } from '../Shared/utils/get-sidebar-icon-tone-rgb';
import { renderHomeVisualIcon } from '../Shared/components/HomeVisualIcon';
import {
APP_FONT,
COLORS,
TABLER_STROKE,
} from '../Shared/utils/home-visual-theme';
import { VISUAL_TOKENS } from '../Shared/utils/home-visual-tokens';
import { renderPreviewIcon } from '../Shared/components/PreviewIcon';
import { MiniIcon } from '../Shared/components/MiniIcon';
import { APP_FONT, COLORS } from '../Shared/utils/app-preview-theme';
import { VISUAL_TOKENS } from '../Shared/utils/app-preview-tokens';
const SidebarItemRow = styled.div<{
$active?: boolean;
@@ -22,7 +19,7 @@ const SidebarItemRow = styled.div<{
$highlighted?: boolean;
$highlightRgb?: string;
}>`
--hero-highlight-rgb: ${({ $highlightRgb }) => $highlightRgb ?? '237, 95, 0'};
--highlight-rgb: ${({ $highlightRgb }) => $highlightRgb ?? '237, 95, 0'};
align-items: center;
background: ${({ $active }) =>
$active ? VISUAL_TOKENS.background.transparent.medium : 'transparent'};
@@ -38,7 +35,7 @@ const SidebarItemRow = styled.div<{
transition: background-color 0.14s ease;
animation: ${({ $highlighted }) =>
$highlighted
? 'heroObjectAppearRow 1800ms cubic-bezier(0.34, 1.56, 0.64, 1) both'
? 'objectAppearRow 1800ms cubic-bezier(0.34, 1.56, 0.64, 1) both'
: 'none'};
transform-origin: left center;
@@ -56,49 +53,49 @@ const SidebarItemRow = styled.div<{
: 'transparent'};
}
@keyframes heroObjectAppearRow {
@keyframes objectAppearRow {
0% {
background: rgba(var(--hero-highlight-rgb, 237, 95, 0), 0);
background: rgba(var(--highlight-rgb, 237, 95, 0), 0);
box-shadow:
0 0 0 0 rgba(var(--hero-highlight-rgb, 237, 95, 0), 0),
0 0 0 0 rgba(var(--hero-highlight-rgb, 237, 95, 0), 0);
0 0 0 0 rgba(var(--highlight-rgb, 237, 95, 0), 0),
0 0 0 0 rgba(var(--highlight-rgb, 237, 95, 0), 0);
opacity: 0;
transform: translateX(-32px) translateY(-6px) scale(0.6);
}
16% {
background: rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.55);
background: rgba(var(--highlight-rgb, 237, 95, 0), 0.55);
box-shadow:
0 0 0 6px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.4),
0 12px 28px -6px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.55);
0 0 0 6px rgba(var(--highlight-rgb, 237, 95, 0), 0.4),
0 12px 28px -6px rgba(var(--highlight-rgb, 237, 95, 0), 0.55);
opacity: 1;
transform: translateX(0) translateY(0) scale(1.18);
}
32% {
background: rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.42);
background: rgba(var(--highlight-rgb, 237, 95, 0), 0.42);
box-shadow:
0 0 0 12px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.24),
0 10px 22px -6px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.38);
0 0 0 12px rgba(var(--highlight-rgb, 237, 95, 0), 0.24),
0 10px 22px -6px rgba(var(--highlight-rgb, 237, 95, 0), 0.38);
transform: translateX(0) scale(0.97);
}
50% {
background: rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.28);
background: rgba(var(--highlight-rgb, 237, 95, 0), 0.28);
box-shadow:
0 0 0 18px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.12),
0 6px 16px -6px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.22);
0 0 0 18px rgba(var(--highlight-rgb, 237, 95, 0), 0.12),
0 6px 16px -6px rgba(var(--highlight-rgb, 237, 95, 0), 0.22);
transform: translateX(0) scale(1.02);
}
72% {
background: rgba(var(--hero-highlight-rgb, 237, 95, 0), 0.16);
background: rgba(var(--highlight-rgb, 237, 95, 0), 0.16);
box-shadow:
0 0 0 22px rgba(var(--hero-highlight-rgb, 237, 95, 0), 0),
0 0 0 0 rgba(var(--hero-highlight-rgb, 237, 95, 0), 0);
0 0 0 22px rgba(var(--highlight-rgb, 237, 95, 0), 0),
0 0 0 0 rgba(var(--highlight-rgb, 237, 95, 0), 0);
transform: translateX(0) scale(1);
}
100% {
background: ${VISUAL_TOKENS.background.transparent.medium};
box-shadow:
0 0 0 0 rgba(var(--hero-highlight-rgb, 237, 95, 0), 0),
0 0 0 0 rgba(var(--hero-highlight-rgb, 237, 95, 0), 0);
0 0 0 0 rgba(var(--highlight-rgb, 237, 95, 0), 0),
0 0 0 0 rgba(var(--highlight-rgb, 237, 95, 0), 0);
transform: translateX(0) scale(1);
}
}
@@ -241,39 +238,20 @@ const SidebarRowMain = styled.div<{ $withBranch?: boolean }>`
padding-left: ${({ $withBranch }) => ($withBranch ? '4px' : '0')};
`;
type ChevronDownMiniProps = {
color?: string;
size?: number;
};
function ChevronDownMini({
color = COLORS.textTertiary,
size = 14,
}: ChevronDownMiniProps) {
return (
<IconChevronDown
aria-hidden
color={color}
size={size}
stroke={TABLER_STROKE}
/>
);
}
type HomeVisualSidebarItemProps = {
type SidebarItemProps = {
collapsible?: boolean;
expanded?: boolean;
depth?: number;
highlightedItemId?: string;
interactive?: boolean;
isLastChild?: boolean;
item: HeroSidebarItem;
item: SidebarItemDef;
onSelect?: (label: string) => void;
onToggleExpanded?: () => void;
selectedLabel?: string;
};
export function HomeVisualSidebarItem({
export function SidebarItem({
collapsible = false,
expanded = false,
depth = 0,
@@ -284,7 +262,7 @@ export function HomeVisualSidebarItem({
onSelect,
onToggleExpanded,
selectedLabel,
}: HomeVisualSidebarItemProps) {
}: SidebarItemProps) {
const showBranch = depth > 0;
const rowSelectable = interactive && item.href === undefined && !collapsible;
const rowInteractive =
@@ -300,7 +278,7 @@ export function HomeVisualSidebarItem({
<>
{showBranch ? <SidebarBranchCell $isLastChild={isLastChild} /> : null}
<SidebarRowMain $withBranch={showBranch}>
{renderHomeVisualIcon(item.icon, rowHighlighted)}
{renderPreviewIcon(item.icon, rowHighlighted)}
<SidebarItemText>
<SidebarItemLabel $active={rowActive}>{item.label}</SidebarItemLabel>
{item.meta ? <SidebarItemMeta>· {item.meta}</SidebarItemMeta> : null}
@@ -308,7 +286,11 @@ export function HomeVisualSidebarItem({
</SidebarRowMain>
{item.showChevron || (item.children && item.children.length > 0) ? (
<SidebarChevron $expanded={!collapsible || expanded}>
<ChevronDownMini color={COLORS.textTertiary} size={12} />
<MiniIcon
icon={IconChevronDown}
color={COLORS.textTertiary}
size={12}
/>
</SidebarChevron>
) : null}
</>
@@ -353,7 +335,7 @@ export function HomeVisualSidebarItem({
<SidebarChildStack>
<BranchLine />
{childItems.map((child, index) => (
<HomeVisualSidebarItem
<SidebarItem
key={child.id}
depth={depth + 1}
highlightedItemId={highlightedItemId}
@@ -1,21 +1,21 @@
import type { HeroSidebarEntry, HeroSidebarItem } from '@/sections/Hero/types';
import type { HeroPageDefaults } from '../Data/hero-page-defaults';
import { normalizeHeroPage } from '../Data/normalize-hero-page';
import type { SidebarEntry, SidebarItemDef } from '../types';
import type { PageDefaults } from '../Data/page-defaults';
import { normalizePage } from '../Data/normalize-page';
import { isFolder } from './is-folder';
function hasRenderablePage(
item: HeroSidebarItem,
pageDefaults: HeroPageDefaults,
item: SidebarItemDef,
pageDefaults: PageDefaults,
): boolean {
return normalizeHeroPage(item, pageDefaults) !== null;
return normalizePage(item, pageDefaults) !== null;
}
export function findActiveItem(
entries: HeroSidebarEntry[],
entries: SidebarEntry[],
activeLabel: string,
pageDefaults: HeroPageDefaults,
): HeroSidebarItem | undefined {
pageDefaults: PageDefaults,
): SidebarItemDef | undefined {
for (const entry of entries) {
if (isFolder(entry)) {
for (const child of entry.items) {
@@ -1,9 +1,9 @@
import type { HeroSidebarEntry } from '@/sections/Hero/types';
import type { SidebarEntry } from '../types';
import { isFolder } from './is-folder';
export function findContainingFolderId(
entries: HeroSidebarEntry[],
entries: SidebarEntry[],
label: string,
): string | undefined {
for (const entry of entries) {
@@ -0,0 +1,5 @@
import type { SidebarEntry, SidebarFolderDef } from '../types';
export function isFolder(entry: SidebarEntry): entry is SidebarFolderDef {
return 'items' in entry;
}
@@ -5,7 +5,7 @@ const loadWorkflowPageModule = () => import('../Pages/Workflow/WorkflowPage');
let deferredPagePreloadPromise: Promise<void> | null = null;
export function preloadDeferredHomeVisualPages() {
export function preloadDeferredPages() {
deferredPagePreloadPromise ??= Promise.all([
loadSalesDashboardPageModule(),
loadWorkflowPageModule(),
@@ -15,7 +15,7 @@ export function preloadDeferredHomeVisualPages() {
deferredPagePreloadPromise = null;
if (process.env.NODE_ENV !== 'production') {
console.error('Home visual deferred page preload failed:', error);
console.error('AppPreview deferred page preload failed:', error);
}
});
@@ -1,8 +1,8 @@
import { useTimeoutRegistry } from '@/lib/react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { HeroSidebarEntry, HeroVisualType } from '@/sections/Hero/types';
import { normalizeHeroPage } from '../Data/normalize-hero-page';
import type { SidebarEntry, AppPreviewConfig } from '../types';
import { normalizePage } from '../Data/normalize-page';
import { findActiveItem } from './find-active-item';
import { findContainingFolderId } from './find-containing-folder-id';
import { isFolder } from './is-folder';
@@ -22,7 +22,7 @@ const COMPLETED_ACTIVE_OBJECT_LABEL =
CRM_OBJECT_SEQUENCE.at(-1)?.label ?? COMPANIES_ITEM_LABEL;
const HIGHLIGHT_RESET_DELAY_MS = 2000;
export function useHomeVisualState(visual: HeroVisualType) {
export function useAppPreviewState(visual: AppPreviewConfig) {
const timeoutRegistry = useTimeoutRegistry();
const defaultActiveLabel =
visual.favoritesNav?.find((item) => item.active)?.label ??
@@ -64,7 +64,7 @@ export function useHomeVisualState(visual: HeroVisualType) {
[visual.actions, visual.tableWidth],
);
const workspaceNav = useMemo<HeroSidebarEntry[]>(() => {
const workspaceNav = useMemo<SidebarEntry[]>(() => {
if (createdObjectIds.length === 0) {
return visual.workspaceNav;
}
@@ -138,7 +138,7 @@ export function useHomeVisualState(visual: HeroVisualType) {
);
const activePage = useMemo(
() => (activeItem ? normalizeHeroPage(activeItem, pageDefaults) : null),
() => (activeItem ? normalizePage(activeItem, pageDefaults) : null),
[activeItem, pageDefaults],
);
@@ -68,19 +68,19 @@ const Shell = styled.div<{
}
`;
type DraggableTerminalProps = {
type TerminalProps = {
onObjectCreated?: (id: string) => void;
onChatFinished?: () => void;
onChatReset?: () => void;
onJumpToConversationEnd?: () => void;
};
export const DraggableTerminal = ({
export const Terminal = ({
onObjectCreated,
onChatFinished,
onChatReset,
onJumpToConversationEnd,
}: DraggableTerminalProps) => {
}: TerminalProps) => {
const {
changeView,
finishChat,

Some files were not shown because too many files have changed in this diff Show More