[Website] Extract HomeVisual into shared AppPreview section. (#20432)
This commit is contained in:
+14
-41
@@ -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
+33
-26
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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);
|
||||
+1
-1
@@ -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;
|
||||
+22
-22
@@ -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;
|
||||
}
|
||||
+7
-9
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
export type HeroPageDefaults = {
|
||||
export type PageDefaults = {
|
||||
defaultActions: string[];
|
||||
defaultTableWidth?: number;
|
||||
};
|
||||
+10
-13
@@ -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,
|
||||
+4
-7
@@ -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>
|
||||
+9
-15
@@ -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
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -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;
|
||||
+22
-11
@@ -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}
|
||||
+41
-36
@@ -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
-1
@@ -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;
|
||||
+2
-2
@@ -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"
|
||||
+2
-2
@@ -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
-1
@@ -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;
|
||||
+1
-1
@@ -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'>
|
||||
+6
-8
@@ -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} />;
|
||||
}
|
||||
+2
-2
@@ -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>
|
||||
);
|
||||
}
|
||||
+10
-10
@@ -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>
|
||||
);
|
||||
}
|
||||
+4
-8
@@ -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
|
||||
+8
-8
@@ -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'
|
||||
+3
-3
@@ -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
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
export function getHomeVisualInitials(value: string) {
|
||||
export function getInitials(value: string) {
|
||||
return value
|
||||
.split(' ')
|
||||
.filter(Boolean)
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+14
-22
@@ -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>
|
||||
+16
-16
@@ -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}
|
||||
+21
-51
@@ -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}
|
||||
</>
|
||||
+13
-15
@@ -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,
|
||||
) {
|
||||
+5
-5
@@ -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>
|
||||
);
|
||||
}
|
||||
+11
-51
@@ -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>
|
||||
+36
-54
@@ -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}
|
||||
+9
-9
@@ -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) {
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
+5
-5
@@ -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],
|
||||
);
|
||||
|
||||
+3
-3
@@ -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
Reference in New Issue
Block a user