polishing next home hero visual (#19284)

**Summary**
- update the home hero navbar controls and surface styling to better
match the latest visual design
- simplify row hover actions by removing edit affordances and disabling
hover controls for `createdBy` and `accountOwner`
- tighten chip typography and spacing for more consistent hero table
rendering
- include the captured Playwright screenshot artifact for reference

**Testing**
- Not run (not requested)
This commit is contained in:
Thomas des Francs
2026-04-02 22:26:05 +02:00
committed by GitHub
parent 04d22feef6
commit 175ae5f0aa
10 changed files with 472 additions and 219 deletions
+1
View File
@@ -52,3 +52,4 @@ mcp.json
/.junie/ /.junie/
TRANSLATION_QA_REPORT.md TRANSLATION_QA_REPORT.md
.playwright-mcp/ .playwright-mcp/
.playwright-cli/
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 89.89 104.6"><defs><clipPath id="a"><path d="M150.97 50.8c-9.07.38-19.66 3.95-25.85 8.6-5.62 4.21-10.26 10.47-8.46 16.56 9.9.19 20.13-3.47 26.07-8.18 5.55-4.4 9.77-10.63 8.24-16.99m3.35 24.48c-15.27 0-21.72 4.49-32.31 4.49-10.92 0-19.23-4.49-32.43-4.49-12.98 0-26.77 4.88-35.51 13.21l-.16.16A24.3 24.3 0 0 0 46.88 102c-.76 4.14-.56 8.66.62 13.35a46 46 0 0 0 5.93 13.35 65 65 0 0 0 11.45 13.35c7 6.45 16.2 13.28 28.04 13.34 11.08.07 14.21-4.37 29.24-4.42s17.87 4.47 28.94 4.4c11.35-.05 20.7-7 27.67-13.32l1.98-1.83a70 70 0 0 0 10.12-11.51l.87-1.18c-9.96-2.32-17.03-6.85-20.85-12.18-3-4.18-4-8.86-2.81-13.35 1.31-4.98 5.3-9.73 12.2-13.35a45 45 0 0 1 6.52-2.76c-8.72-6.72-20.95-10.61-32.48-10.61"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(.61862 0 0 1 -28.72 -50.8)"><g stroke-linecap="round"><path fill="#75bd21" d="M24.86 37.45H213.3V102H24.86z"/><path fill="#ffc728" d="M24.86 88.65H213.3v26.7H24.86z"/><path fill="#ff661c" d="M24.86 102H213.3v26.7H24.86z"/><path fill="#cf0f2b" d="M24.86 115.35H213.3v26.7H24.86z"/><path fill="#b01cab" d="M24.86 128.7H213.3v26.68H24.86z"/><path fill="#00a1de" d="M24.86 142.06H213.3v26.68H24.86z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -8,7 +8,7 @@ const PEOPLE_AVATAR_URLS = {
jeffWilliams: jeffWilliams:
'https://twentyhq.github.io/placeholder-images/people/image-22.png', 'https://twentyhq.github.io/placeholder-images/people/image-22.png',
katherineAdams: katherineAdams:
'https://twentyhq.github.io/placeholder-images/people/image-07.png', '/images/home/hero/avatars/katherine-adams.jpg',
philSchiller: philSchiller:
'https://twentyhq.github.io/placeholder-images/people/image-14.png', 'https://twentyhq.github.io/placeholder-images/people/image-14.png',
timCook: timCook:
@@ -28,66 +28,6 @@ export const HERO_DATA: HeroHomeDataType = {
workspace: { icon: 'apple', name: 'Apple' }, workspace: { icon: 'apple', name: 'Apple' },
tableWidth: 1700, tableWidth: 1700,
actions: ['Filter', 'Sort', 'Options'], actions: ['Filter', 'Sort', 'Options'],
favoritesNav: [
{
id: 'fav-to-follow',
label: 'To Follow',
icon: { kind: 'tabler', name: 'folder', tone: 'orange' },
showChevron: true,
children: [
{
id: 'fav-stripe',
label: 'Stripe',
meta: 'Company',
icon: { kind: 'brand', brand: 'stripe' },
},
{
id: 'fav-airbnb',
label: 'Airbnb',
meta: 'Company',
icon: { kind: 'brand', brand: 'airbnb' },
},
{
id: 'fav-brian-chesky',
label: 'Brian Chesky',
meta: 'Person',
icon: {
kind: 'avatar',
label: 'B',
tone: 'violet',
shape: 'circle',
},
},
],
},
{
id: 'fav-all-companies',
label: 'All Companies',
icon: { kind: 'tabler', name: 'buildingSkyscraper', tone: 'blue' },
},
{
id: 'fav-send-nda',
label: 'Send NDA to Qonto - T...',
icon: { kind: 'avatar', label: 'L', tone: 'teal', shape: 'circle' },
},
{
id: 'fav-page-layout',
label: 'Page Layout',
icon: { kind: 'brand', brand: 'page-layout' },
},
{
id: 'fav-figma',
label: 'Figma',
meta: 'Company',
icon: { kind: 'brand', brand: 'figma' },
},
{
id: 'fav-ben-chestnut',
label: 'Ben Chestnut',
meta: 'Person',
icon: { kind: 'brand', brand: 'ben-chestnut' },
},
],
workspaceNav: [ workspaceNav: [
{ {
id: 'companies', id: 'companies',
@@ -369,7 +309,7 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'people', id: 'people',
label: 'People', label: 'People',
icon: { kind: 'tabler', name: 'user', tone: 'purple' }, icon: { kind: 'tabler', name: 'user', tone: 'blue' },
viewLabel: 'All People', viewLabel: 'All People',
viewCount: 5, viewCount: 5,
columns: [ columns: [
@@ -414,7 +354,7 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'opportunities', id: 'opportunities',
label: 'Opportunities', label: 'Opportunities',
icon: { kind: 'tabler', name: 'targetArrow', tone: 'pink' }, icon: { kind: 'tabler', name: 'targetArrow', tone: 'red' },
viewLabel: 'All Opportunities', viewLabel: 'All Opportunities',
viewCount: 3, viewCount: 3,
columns: [ columns: [
@@ -429,7 +369,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'enterprise-rollout', id: 'enterprise-rollout',
cells: { cells: {
name: { type: 'text', value: 'Enterprise rollout' }, name: {
type: 'text',
value: 'Enterprise rollout',
shortLabel: 'E',
tone: 'pink',
},
company: { type: 'entity', name: 'Slack', domain: 'slack.com' }, company: { type: 'entity', name: 'Slack', domain: 'slack.com' },
amount: { type: 'number', value: '$2,300,000' }, amount: { type: 'number', value: '$2,300,000' },
stage: { type: 'tag', value: 'Negotiation' }, stage: { type: 'tag', value: 'Negotiation' },
@@ -440,7 +385,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'workspace-upgrade', id: 'workspace-upgrade',
cells: { cells: {
name: { type: 'text', value: 'Workspace upgrade' }, name: {
type: 'text',
value: 'Workspace upgrade',
shortLabel: 'W',
tone: 'pink',
},
company: { type: 'entity', name: 'Notion', domain: 'notion.com' }, company: { type: 'entity', name: 'Notion', domain: 'notion.com' },
amount: { type: 'number', value: '$750,000' }, amount: { type: 'number', value: '$750,000' },
stage: { type: 'tag', value: 'Qualified' }, stage: { type: 'tag', value: 'Qualified' },
@@ -467,7 +417,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'send-nda', id: 'send-nda',
cells: { cells: {
title: { type: 'text', value: 'Send NDA' }, title: {
type: 'text',
value: 'Send NDA',
shortLabel: 'S',
tone: 'teal',
},
assignee: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, assignee: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook },
dueDate: { type: 'text', value: 'Oct 25, 2023' }, dueDate: { type: 'text', value: 'Oct 25, 2023' },
relatedTo: { type: 'entity', name: 'Anthropic', domain: 'anthropic.com' }, relatedTo: { type: 'entity', name: 'Anthropic', domain: 'anthropic.com' },
@@ -477,7 +432,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'review-proposal', id: 'review-proposal',
cells: { cells: {
title: { type: 'text', value: 'Review proposal' }, title: {
type: 'text',
value: 'Review proposal',
shortLabel: 'R',
tone: 'teal',
},
assignee: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue }, assignee: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue },
dueDate: { type: 'text', value: 'Oct 28, 2023' }, dueDate: { type: 'text', value: 'Oct 28, 2023' },
relatedTo: { type: 'entity', name: 'Slack', domain: 'slack.com' }, relatedTo: { type: 'entity', name: 'Slack', domain: 'slack.com' },
@@ -489,7 +449,7 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'notes', id: 'notes',
label: 'Notes', label: 'Notes',
icon: { kind: 'tabler', name: 'notes', tone: 'green' }, icon: { kind: 'tabler', name: 'notes', tone: 'teal' },
viewLabel: 'All Notes', viewLabel: 'All Notes',
viewCount: 2, viewCount: 2,
columns: [ columns: [
@@ -502,7 +462,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'discovery-call', id: 'discovery-call',
cells: { cells: {
title: { type: 'text', value: 'Discovery call notes' }, title: {
type: 'text',
value: 'Discovery call notes',
shortLabel: 'D',
tone: 'green',
},
createdBy: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, createdBy: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller },
relatedTo: { type: 'entity', name: 'Notion', domain: 'notion.com' }, relatedTo: { type: 'entity', name: 'Notion', domain: 'notion.com' },
added: { type: 'text', value: 'Sep 2, 2023' }, added: { type: 'text', value: 'Sep 2, 2023' },
@@ -511,7 +476,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'design-system-meeting', id: 'design-system-meeting',
cells: { cells: {
title: { type: 'text', value: 'Design system meeting' }, title: {
type: 'text',
value: 'Design system meeting',
shortLabel: 'D',
tone: 'green',
},
createdBy: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, createdBy: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook },
relatedTo: { type: 'entity', name: 'Figma', domain: 'figma.com' }, relatedTo: { type: 'entity', name: 'Figma', domain: 'figma.com' },
added: { type: 'text', value: 'Oct 18, 2023' }, added: { type: 'text', value: 'Oct 18, 2023' },
@@ -522,7 +492,7 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'sales-dashboard', id: 'sales-dashboard',
label: 'Sales Dashboard', label: 'Sales Dashboard',
icon: { kind: 'avatar', label: '$', tone: 'amber', shape: 'circle' }, icon: { kind: 'avatar', label: 'S', tone: 'amber', shape: 'circle' },
meta: 'Dashboard', meta: 'Dashboard',
viewLabel: 'Sales Dashboard', viewLabel: 'Sales Dashboard',
viewCount: 0, viewCount: 0,
@@ -550,7 +520,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'new-lead', id: 'new-lead',
cells: { cells: {
name: { type: 'text', value: 'New Lead Assignment' }, name: {
type: 'text',
value: 'New Lead Assignment',
shortLabel: 'N',
tone: 'amber',
},
status: { type: 'tag', value: 'Active' }, status: { type: 'tag', value: 'Active' },
lastRun: { type: 'text', value: 'Oct 24, 2023 10:00 am' }, lastRun: { type: 'text', value: 'Oct 24, 2023 10:00 am' },
}, },
@@ -558,7 +533,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'nurture', id: 'nurture',
cells: { cells: {
name: { type: 'text', value: 'Nurture Sequence' }, name: {
type: 'text',
value: 'Nurture Sequence',
shortLabel: 'N',
tone: 'amber',
},
status: { type: 'tag', value: 'Inactive' }, status: { type: 'tag', value: 'Inactive' },
lastRun: { type: 'text', value: 'Oct 20, 2023 3:15 pm' }, lastRun: { type: 'text', value: 'Oct 20, 2023 3:15 pm' },
}, },
@@ -582,7 +562,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'run-12345', id: 'run-12345',
cells: { cells: {
runId: { type: 'text', value: 'run_12345' }, runId: {
type: 'text',
value: 'run_12345',
shortLabel: 'R',
tone: 'amber',
},
workflow: { type: 'text', value: 'New Lead Assignment' }, workflow: { type: 'text', value: 'New Lead Assignment' },
status: { type: 'tag', value: 'Success' }, status: { type: 'tag', value: 'Success' },
startedAt: { type: 'text', value: 'Oct 24, 2023 10:00 am' }, startedAt: { type: 'text', value: 'Oct 24, 2023 10:00 am' },
@@ -592,7 +577,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'run-12346', id: 'run-12346',
cells: { cells: {
runId: { type: 'text', value: 'run_12346' }, runId: {
type: 'text',
value: 'run_12346',
shortLabel: 'R',
tone: 'amber',
},
workflow: { type: 'text', value: 'Nurture Sequence' }, workflow: { type: 'text', value: 'Nurture Sequence' },
status: { type: 'tag', value: 'Failed' }, status: { type: 'tag', value: 'Failed' },
startedAt: { type: 'text', value: 'Oct 20, 2023 3:15 pm' }, startedAt: { type: 'text', value: 'Oct 20, 2023 3:15 pm' },
@@ -617,7 +607,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'v2-lead', id: 'v2-lead',
cells: { cells: {
version: { type: 'text', value: 'v2' }, version: {
type: 'text',
value: 'v2',
shortLabel: 'V',
tone: 'amber',
},
workflow: { type: 'text', value: 'New Lead Assignment' }, workflow: { type: 'text', value: 'New Lead Assignment' },
publishedAt: { type: 'text', value: 'Oct 15, 2023 9:00 am' }, publishedAt: { type: 'text', value: 'Oct 15, 2023 9:00 am' },
publishedBy: { type: 'person', name: 'Ivan Zhao', shortLabel: 'I', tone: 'gray', kind: 'person', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' }, publishedBy: { type: 'person', name: 'Ivan Zhao', shortLabel: 'I', tone: 'gray', kind: 'person', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' },
@@ -626,7 +621,12 @@ export const HERO_DATA: HeroHomeDataType = {
{ {
id: 'v1-lead', id: 'v1-lead',
cells: { cells: {
version: { type: 'text', value: 'v1' }, version: {
type: 'text',
value: 'v1',
shortLabel: 'V',
tone: 'amber',
},
workflow: { type: 'text', value: 'New Lead Assignment' }, workflow: { type: 'text', value: 'New Lead Assignment' },
publishedAt: { type: 'text', value: 'Sep 10, 2023 1:00 pm' }, publishedAt: { type: 'text', value: 'Sep 10, 2023 1:00 pm' },
publishedBy: { type: 'person', name: 'Ivan Zhao', shortLabel: 'I', tone: 'gray', kind: 'person', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' }, publishedBy: { type: 'person', name: 'Ivan Zhao', shortLabel: 'I', tone: 'gray', kind: 'person', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' },
@@ -637,15 +637,15 @@ export const HERO_DATA: HeroHomeDataType = {
], ],
}, },
{ {
id: 'claude', id: 'book-demo',
label: 'Claude', label: 'Book a demo',
icon: { kind: 'brand', brand: 'claude', overlay: 'link' }, href: 'https://cal.com/forms/f7841033-0a20-4958-8c92-4e34ec128a81',
}, icon: {
{ kind: 'brand',
id: 'workspace-stripe', brand: 'twenty',
label: 'Stripe', imageSrc: '/images/home/hero/twenty-demo-logo.png',
icon: { kind: 'brand', brand: 'stripe' }, overlay: 'link',
showChevron: true, },
}, },
], ],
}, },
@@ -5,7 +5,6 @@ import { styled } from '@linaria/react';
import { import {
IconBook, IconBook,
IconBox, IconBox,
IconBrandApple,
IconBrandLinkedin, IconBrandLinkedin,
IconBuildingFactory2, IconBuildingFactory2,
IconBuildingSkyscraper, IconBuildingSkyscraper,
@@ -52,6 +51,7 @@ import type {
HeroCellEntity, HeroCellEntity,
HeroCellPerson, HeroCellPerson,
HeroCellRelation, HeroCellRelation,
HeroCellText,
HeroCellValue, HeroCellValue,
HeroColumnDef, HeroColumnDef,
HeroRowDef, HeroRowDef,
@@ -66,6 +66,9 @@ import { VISUAL_TOKENS } from './homeVisualTokens';
const APP_FONT = VISUAL_TOKENS.font.family; const APP_FONT = VISUAL_TOKENS.font.family;
const DEFAULT_TABLE_WIDTH = 1700; const DEFAULT_TABLE_WIDTH = 1700;
const APPLE_WORKSPACE_LOGO_SRC = '/images/home/hero/apple-rainbow-logo.svg';
const TABLE_CELL_HORIZONTAL_PADDING = 8;
const HOVER_ACTION_EDGE_INSET = 4;
const COLORS = { const COLORS = {
accent: VISUAL_TOKENS.accent.accent9, accent: VISUAL_TOKENS.accent.accent9,
@@ -88,16 +91,16 @@ const SIDEBAR_TONES: Record<
string, string,
{ background: string; border: string; color: string } { background: string; border: string; color: string }
> = { > = {
amber: { background: '#fff4d6', border: '#ffd49b', color: '#9a6700' }, amber: { background: '#FEF2A4', border: '#FEF2A4', color: '#35290F' },
blue: { background: '#d9e2fc', border: '#c6d4f9', color: '#3557c6' }, blue: { background: '#d9e2fc', border: '#c6d4f9', color: '#3A5CCC' },
gray: { background: '#ebebeb', border: '#d6d6d6', color: '#666666' }, gray: { background: '#ebebeb', border: '#d6d6d6', color: '#838383' },
green: { background: '#ccebd7', border: '#bbe4c9', color: '#153226' }, green: { background: '#ccebd7', border: '#bbe4c9', color: '#153226' },
orange: { background: '#ffdcc3', border: '#ffcca7', color: '#582d1d' }, orange: { background: '#ffdcc3', border: '#ffcca7', color: '#ED5F00' },
pink: { background: '#ffe1e7', border: '#ffc8d6', color: '#a51853' }, pink: { background: '#ffe1e7', border: '#ffc8d6', color: '#a51853' },
purple: { background: '#e0e7ff', border: '#c7d2fe', color: '#4f46e5' }, purple: { background: '#e0e7ff', border: '#c7d2fe', color: '#4f46e5' },
teal: { background: '#c7ebe5', border: '#b3e3dc', color: '#10302b' }, teal: { background: '#c7ebe5', border: '#afdfd7', color: '#0E9888' },
violet: { background: '#ebe5ff', border: '#d8cbff', color: '#5b3fd1' }, violet: { background: '#ebe5ff', border: '#d8cbff', color: '#5b3fd1' },
red: { background: '#fee2e2', border: '#fecaca', color: '#b91c1c' }, red: { background: '#fdd8d8', border: '#f9c6c6', color: '#DC3D43' },
}; };
const PERSON_TONES: Record<string, { background: string; color: string }> = { const PERSON_TONES: Record<string, { background: string; color: string }> = {
@@ -113,6 +116,10 @@ const PERSON_TONES: Record<string, { background: string; color: string }> = {
const TABLER_STROKE = 1.6; const TABLER_STROKE = 1.6;
const NAVIGATION_TABLER_STROKE = 2; const NAVIGATION_TABLER_STROKE = 2;
const ROW_HOVER_ACTION_DISABLED_COLUMNS = new Set([
'createdBy',
'accountOwner',
]);
// -- Styled Components -- // -- Styled Components --
@@ -139,16 +146,7 @@ const ShellScene = styled.div`
const Frame = styled.div` const Frame = styled.div`
aspect-ratio: 1280 / 832; aspect-ratio: 1280 / 832;
background-color: ${COLORS.background}; background-color: ${COLORS.background};
background-image: radial-gradient( background-image: ${VISUAL_TOKENS.background.noisy};
circle at top center,
rgba(0, 0, 0, 0.035),
rgba(0, 0, 0, 0) 55%
),
${VISUAL_TOKENS.background.noisy};
background-position:
center top,
center;
background-repeat: no-repeat, repeat;
border: 1px solid ${COLORS.border}; border: 1px solid ${COLORS.border};
border-radius: 8px; border-radius: 8px;
box-shadow: ${COLORS.shadow}; box-shadow: ${COLORS.shadow};
@@ -157,24 +155,27 @@ const Frame = styled.div`
`; `;
const AppLayout = styled.div` const AppLayout = styled.div`
display: grid; display: flex;
grid-template-columns: 72px minmax(0, 1fr);
height: 100%; height: 100%;
min-height: 0;
position: relative; position: relative;
z-index: 1; z-index: 1;
@media (min-width: ${theme.breakpoints.md}px) {
grid-template-columns: 220px minmax(0, 1fr);
}
`; `;
const SidebarPanel = styled.aside` const SidebarPanel = styled.aside`
background: transparent; background: transparent;
border-right: 1px solid rgba(0, 0, 0, 0.04);
display: grid; display: grid;
flex: 0 0 72px;
gap: 12px; gap: 12px;
grid-template-rows: auto auto minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr);
min-height: 0;
padding: 12px 8px; padding: 12px 8px;
width: 72px;
@media (min-width: ${theme.breakpoints.md}px) {
flex-basis: 220px;
width: 220px;
}
`; `;
const SidebarTopBar = styled.div` const SidebarTopBar = styled.div`
@@ -196,16 +197,17 @@ const WorkspaceMenu = styled.div`
const WorkspaceIcon = styled.div` const WorkspaceIcon = styled.div`
align-items: center; align-items: center;
background: #111111;
border-radius: 2px;
color: #ffffff;
display: flex; display: flex;
font-family: ${APP_FONT}; flex: 0 0 auto;
font-size: 11px;
font-weight: ${theme.font.weight.medium};
height: 16px; height: 16px;
justify-content: center; justify-content: center;
width: 16px; width: 14px;
`;
const WorkspaceIconImage = styled.img`
display: block;
height: 100%;
width: 100%;
`; `;
const WorkspaceLabel = styled.span` const WorkspaceLabel = styled.span`
@@ -246,10 +248,17 @@ const SidebarControls = styled.div`
display: grid; display: grid;
gap: 8px; gap: 8px;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
min-width: 0;
@media (min-width: ${theme.breakpoints.md}px) {
display: flex;
gap: 12px;
justify-content: space-between;
}
`; `;
const SegmentedRail = styled.div` const SegmentedRail = styled.div`
background: rgba(252, 252, 252, 0.8); background: #fcfcfccc;
border: 1px solid ${COLORS.border}; border: 1px solid ${COLORS.border};
border-radius: 40px; border-radius: 40px;
display: grid; display: grid;
@@ -261,12 +270,17 @@ const SegmentedRail = styled.div`
const Segment = styled.div<{ $selected?: boolean }>` const Segment = styled.div<{ $selected?: boolean }>`
align-items: center; align-items: center;
background: ${({ $selected }) => background: ${({ $selected }) =>
$selected ? 'rgba(0, 0, 0, 0.04)' : 'transparent'}; $selected ? '#0000000a' : 'transparent'};
border-radius: 16px; border-radius: 16px;
display: flex; display: flex;
height: 22px; height: 22px;
justify-content: center; justify-content: center;
width: 22px; width: 22px;
@media (min-width: ${theme.breakpoints.md}px) {
padding: 0 8px;
width: 32px;
}
`; `;
const NewChat = styled.div` const NewChat = styled.div`
@@ -305,8 +319,9 @@ const NewChatLabel = styled.span`
`; `;
const SidebarScroll = styled.div` const SidebarScroll = styled.div`
display: grid; display: flex;
gap: 12px; flex-direction: column;
gap: 2px;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
`; `;
@@ -316,14 +331,15 @@ const SidebarSection = styled.div`
gap: 2px; gap: 2px;
`; `;
const SidebarSectionLabel = styled.span` const SidebarSectionLabel = styled.span<{ $workspace?: boolean }>`
color: ${COLORS.textLight}; color: ${COLORS.textLight};
display: none; display: none;
font-family: ${APP_FONT}; font-family: ${APP_FONT};
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
padding: 0 4px 4px; padding: ${({ $workspace }) =>
$workspace ? '4px 4px 8px' : '0 4px 4px'};
@media (min-width: ${theme.breakpoints.md}px) { @media (min-width: ${theme.breakpoints.md}px) {
display: block; display: block;
@@ -333,17 +349,57 @@ const SidebarSectionLabel = styled.span`
const SidebarItemRow = styled.div<{ const SidebarItemRow = styled.div<{
$active?: boolean; $active?: boolean;
$depth?: number; $depth?: number;
$interactive?: boolean;
$withBranch?: boolean;
}>` }>`
align-items: center; align-items: center;
background: ${({ $active }) => background: ${({ $active }) =>
$active ? 'rgba(0, 0, 0, 0.04)' : 'transparent'}; $active ? VISUAL_TOKENS.background.transparent.medium : 'transparent'};
border-radius: 4px; border-radius: 4px;
display: grid; display: grid;
gap: 8px; gap: 0;
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: ${({ $withBranch }) =>
$withBranch ? '9px minmax(0, 1fr) auto' : 'minmax(0, 1fr) auto'};
height: 28px; height: 28px;
padding: 0 2px 0 ${({ $depth = 0 }) => `${$depth === 0 ? 4 : 24}px`}; padding: 0 2px 0 ${({ $depth = 0 }) => `${$depth === 0 ? 4 : 11}px`};
position: relative; position: relative;
text-decoration: none;
transition: background-color 0.14s ease;
&:hover {
background: ${({ $active, $interactive }) =>
$active || $interactive
? VISUAL_TOKENS.background.transparent.medium
: 'transparent'};
}
`;
const SidebarItemRowLink = styled.a<{
$active?: boolean;
$depth?: number;
$interactive?: boolean;
$withBranch?: boolean;
}>`
align-items: center;
background: ${({ $active }) =>
$active ? VISUAL_TOKENS.background.transparent.medium : 'transparent'};
border-radius: 4px;
display: grid;
gap: 0;
grid-template-columns: ${({ $withBranch }) =>
$withBranch ? '9px minmax(0, 1fr) auto' : 'minmax(0, 1fr) auto'};
height: 28px;
padding: 0 2px 0 ${({ $depth = 0 }) => `${$depth === 0 ? 4 : 11}px`};
position: relative;
text-decoration: none;
transition: background-color 0.14s ease;
&:hover {
background: ${({ $active, $interactive }) =>
$active || $interactive
? VISUAL_TOKENS.background.transparent.medium
: 'transparent'};
}
`; `;
const SidebarIconSurface = styled.div<{ const SidebarIconSurface = styled.div<{
@@ -371,8 +427,8 @@ const SidebarItemText = styled.div`
min-width: 0; min-width: 0;
`; `;
const SidebarItemLabel = styled.span` const SidebarItemLabel = styled.span<{ $active?: boolean }>`
color: ${COLORS.textSecondary}; color: ${({ $active }) => ($active ? COLORS.text : COLORS.textSecondary)};
display: none; display: none;
font-family: ${APP_FONT}; font-family: ${APP_FONT};
font-size: 13px; font-size: 13px;
@@ -423,12 +479,43 @@ const SidebarChildStack = styled.div`
const BranchLine = styled.div` const BranchLine = styled.div`
background: ${COLORS.borderStrong}; background: ${COLORS.borderStrong};
bottom: 14px; bottom: 14px;
left: 15px; left: 11px;
position: absolute; position: absolute;
top: 0; top: 0;
width: 1px; width: 1px;
`; `;
const SidebarBranchCell = styled.div<{ $isLastChild?: boolean }>`
align-self: stretch;
position: relative;
width: 9px;
&::before {
background: ${COLORS.borderStrong};
content: '';
inset: 0 88.89% 0 0;
opacity: ${({ $isLastChild }) => ($isLastChild ? 0 : 1)};
position: absolute;
}
&::after {
border-bottom: 1px solid ${COLORS.borderStrong};
border-left: 1px solid ${COLORS.borderStrong};
border-radius: 0 0 0 4px;
content: '';
inset: 0 0 45.83% 0;
position: absolute;
}
`;
const SidebarRowMain = styled.div<{ $withBranch?: boolean }>`
align-items: center;
display: flex;
gap: 8px;
min-width: 0;
padding-left: ${({ $withBranch }) => ($withBranch ? '4px' : '0')};
`;
const SidebarAvatar = styled.div<{ const SidebarAvatar = styled.div<{
$background: string; $background: string;
$color: string; $color: string;
@@ -450,9 +537,11 @@ const SidebarAvatar = styled.div<{
`; `;
const RightPane = styled.div` const RightPane = styled.div`
display: grid; display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 12px; gap: 12px;
grid-template-rows: 32px minmax(0, 1fr); min-height: 0;
min-width: 0; min-width: 0;
padding: 12px 8px 12px 0; padding: 12px 8px 12px 0;
@@ -465,6 +554,8 @@ const NavbarBar = styled.div`
align-items: center; align-items: center;
background: transparent; background: transparent;
display: flex; display: flex;
flex: 0 0 32px;
height: 32px;
justify-content: space-between; justify-content: space-between;
min-width: 0; min-width: 0;
`; `;
@@ -524,12 +615,13 @@ const DesktopOnlyNavbarAction = styled.div`
} }
`; `;
const NavbarDecorativeChip = styled.div` const NAVBAR_ACTION_BORDER = 'rgba(0, 0, 0, 0.08)';
const NavbarActionButton = styled.div`
align-items: center; align-items: center;
background: transparent; background: transparent;
border: 1px solid ${VISUAL_TOKENS.background.transparent.medium}; border: 1px solid ${NAVBAR_ACTION_BORDER};
border-radius: ${VISUAL_TOKENS.border.radius.sm}; border-radius: ${VISUAL_TOKENS.border.radius.sm};
color: ${VISUAL_TOKENS.font.color.secondary};
display: inline-flex; display: inline-flex;
font-family: ${APP_FONT}; font-family: ${APP_FONT};
font-size: ${VISUAL_TOKENS.font.size.md}; font-size: ${VISUAL_TOKENS.font.size.md};
@@ -540,19 +632,38 @@ const NavbarDecorativeChip = styled.div`
white-space: nowrap; white-space: nowrap;
`; `;
const NavbarDecorativeIconWrap = styled.span` const NavbarActionIconWrap = styled.span<{ $color?: string }>`
align-items: center; align-items: center;
color: currentColor; color: ${({ $color }) => $color ?? VISUAL_TOKENS.font.color.secondary};
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;
justify-content: center; justify-content: center;
`; `;
const NavbarActionLabel = styled.span<{ $color?: string }>`
color: ${({ $color }) => $color ?? VISUAL_TOKENS.font.color.secondary};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
line-height: 1.4;
white-space: nowrap;
`;
const NavbarActionSeparator = styled.div`
background: ${VISUAL_TOKENS.background.transparent.medium};
border-radius: 56px;
height: 100%;
width: 1px;
`;
const IndexSurface = styled.div` const IndexSurface = styled.div`
background: ${COLORS.background}; background: ${COLORS.background};
border: 1px solid ${COLORS.border};
border-radius: 8px; border-radius: 8px;
display: grid; display: flex;
grid-template-rows: 40px minmax(0, 1fr); flex: 1 1 auto;
flex-direction: column;
min-height: 0;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
`; `;
@@ -563,15 +674,19 @@ const ViewbarBar = styled.div`
border-bottom: 1px solid ${COLORS.borderLight}; border-bottom: 1px solid ${COLORS.borderLight};
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
min-width: 0;
padding: 8px 8px 8px 12px; padding: 8px 8px 8px 12px;
width: 100%;
`; `;
const ViewSwitcher = styled.div` const ViewSwitcher = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
flex: 1 1 auto;
gap: 4px; gap: 4px;
height: 24px; height: 24px;
min-width: 0; min-width: 0;
overflow: hidden;
padding: 0 4px; padding: 0 4px;
`; `;
@@ -603,7 +718,11 @@ const TinyDot = styled.div`
const ViewActions = styled.div` const ViewActions = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
flex: 0 0 auto;
gap: 2px; gap: 2px;
margin-left: auto;
position: relative;
z-index: 1;
`; `;
const ViewAction = styled.span` const ViewAction = styled.span`
@@ -622,7 +741,11 @@ const ViewAction = styled.span`
const TableShell = styled.div` const TableShell = styled.div`
display: flex; display: flex;
flex: 1 1 auto;
min-width: 0;
min-height: 0; min-height: 0;
overflow: hidden;
width: 100%;
`; `;
const GripRail = styled.div` const GripRail = styled.div`
@@ -640,11 +763,14 @@ const GripCell = styled.div`
const TableViewport = styled.div<{ $dragging: boolean }>` const TableViewport = styled.div<{ $dragging: boolean }>`
cursor: ${({ $dragging }) => ($dragging ? 'grabbing' : 'grab')}; cursor: ${({ $dragging }) => ($dragging ? 'grabbing' : 'grab')};
flex: 1 1 auto;
min-height: 0; min-height: 0;
min-width: 0;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
overscroll-behavior-x: contain; overscroll-behavior-x: contain;
scrollbar-width: none; scrollbar-width: none;
width: 100%;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
@@ -652,6 +778,10 @@ const TableViewport = styled.div<{ $dragging: boolean }>`
`; `;
const TableCanvas = styled.div<{ $width: number }>` const TableCanvas = styled.div<{ $width: number }>`
display: flex;
flex-direction: column;
height: 100%;
min-height: 100%;
min-width: ${({ $width }) => `${$width}px`}; min-width: ${({ $width }) => `${$width}px`};
width: ${({ $width }) => `${$width}px`}; width: ${({ $width }) => `${$width}px`};
`; `;
@@ -693,7 +823,7 @@ const TableCell = styled.div<{
$align === 'right' ? 'flex-end' : 'flex-start'}; $align === 'right' ? 'flex-end' : 'flex-start'};
left: ${({ $sticky }) => ($sticky ? '0' : 'auto')}; left: ${({ $sticky }) => ($sticky ? '0' : 'auto')};
min-width: ${({ $width }) => `${$width}px`}; min-width: ${({ $width }) => `${$width}px`};
padding: 0 8px; padding: 0 ${TABLE_CELL_HORIZONTAL_PADDING}px;
position: ${({ $sticky }) => ($sticky ? 'sticky' : 'relative')}; position: ${({ $sticky }) => ($sticky ? 'sticky' : 'relative')};
z-index: ${({ $header, $sticky }) => { z-index: ${({ $header, $sticky }) => {
if ($sticky && $header) { if ($sticky && $header) {
@@ -778,6 +908,16 @@ const EntityCellLayout = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
gap: 4px; gap: 4px;
height: 100%;
min-width: 0;
position: relative;
width: 100%;
`;
const CellHoverAnchor = styled.div`
align-items: center;
display: flex;
height: 100%;
min-width: 0; min-width: 0;
position: relative; position: relative;
width: 100%; width: 100%;
@@ -826,7 +966,7 @@ const PersonAvatarCircle = styled.div<{
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;
font-family: ${APP_FONT}; font-family: ${APP_FONT};
font-size: 9px; font-size: 10px;
font-weight: ${theme.font.weight.medium}; font-weight: ${theme.font.weight.medium};
height: 14px; height: 14px;
justify-content: center; justify-content: center;
@@ -847,24 +987,29 @@ const BooleanRow = styled.div`
gap: 4px; gap: 4px;
`; `;
const HoverActions = styled.div<{ $visible: boolean }>` const HoverActions = styled.div<{ $rightInset?: number; $visible: boolean }>`
align-items: center; align-items: center;
background: ${COLORS.backgroundSecondary}; background: ${VISUAL_TOKENS.background.transparent.primary};
border: 1px solid ${COLORS.border}; border: 1px solid ${VISUAL_TOKENS.background.transparent.light};
border-radius: 4px; border-radius: 4px;
bottom: 4px;
box-sizing: border-box;
box-shadow: ${VISUAL_TOKENS.boxShadow.light}; box-shadow: ${VISUAL_TOKENS.boxShadow.light};
display: flex; display: flex;
gap: 2px; gap: 0;
justify-content: center;
opacity: ${({ $visible }) => ($visible ? 1 : 0)}; opacity: ${({ $visible }) => ($visible ? 1 : 0)};
padding: 2px; padding: 0 4px;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
right: 4px; right: ${({ $rightInset = HOVER_ACTION_EDGE_INSET - TABLE_CELL_HORIZONTAL_PADDING }) =>
`${$rightInset}px`};
top: 4px; top: 4px;
transform: translateX(${({ $visible }) => ($visible ? '0' : '4px')}); transform: translateX(${({ $visible }) => ($visible ? '0' : '4px')});
transition: transition:
opacity 0.14s ease, opacity 0.14s ease,
transform 0.14s ease; transform 0.14s ease;
width: 24px;
`; `;
const MiniAction = styled.div` const MiniAction = styled.div`
@@ -872,9 +1017,9 @@ const MiniAction = styled.div`
border-radius: 2px; border-radius: 2px;
color: ${COLORS.textSecondary}; color: ${COLORS.textSecondary};
display: flex; display: flex;
height: 20px; height: 16px;
justify-content: center; justify-content: center;
width: 20px; width: 16px;
`; `;
const FooterFirstContent = styled.div` const FooterFirstContent = styled.div`
@@ -960,6 +1105,7 @@ const HEADER_ICON_MAP: Record<string, typeof IconBuildingSkyscraper> = {
// -- Utility functions -- // -- Utility functions --
const failedAvatarUrls = new Set<string>();
const failedFaviconUrls = new Set<string>(); const failedFaviconUrls = new Set<string>();
function getInitials(value: string) { function getInitials(value: string) {
@@ -1157,15 +1303,17 @@ function CopyMini({ color = COLORS.textSecondary, size = 14 }: MiniIconProps) {
// -- Favicon logo component -- // -- Favicon logo component --
function FaviconLogo({ function FaviconLogo({
src,
domain, domain,
label, label,
size = 14, size = 14,
}: { }: {
src?: string;
domain?: string; domain?: string;
label?: string; label?: string;
size?: number; size?: number;
}) { }) {
const faviconUrl = getLogoUrlFromDomainName(domain); const faviconUrl = src ?? getLogoUrlFromDomainName(domain);
const [localFailedUrl, setLocalFailedUrl] = useState<string | null>(null); const [localFailedUrl, setLocalFailedUrl] = useState<string | null>(null);
const showFavicon = const showFavicon =
faviconUrl !== undefined && faviconUrl !== undefined &&
@@ -1213,6 +1361,31 @@ function FaviconLogo({
// -- Sidebar icon rendering -- // -- Sidebar icon rendering --
function PersonAvatarContent({ token }: { token: HeroCellPerson }) {
const [localFailedUrl, setLocalFailedUrl] = useState<string | null>(null);
const showAvatar =
token.avatarUrl !== undefined &&
!failedAvatarUrls.has(token.avatarUrl) &&
localFailedUrl !== token.avatarUrl;
if (showAvatar) {
return (
<AvatarImage
alt=""
src={token.avatarUrl}
onError={() => {
if (token.avatarUrl) {
failedAvatarUrls.add(token.avatarUrl);
setLocalFailedUrl(token.avatarUrl);
}
}}
/>
);
}
return token.shortLabel ?? getInitials(token.name);
}
function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode { function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode {
if (icon.kind === 'brand') { if (icon.kind === 'brand') {
return ( return (
@@ -1223,14 +1396,16 @@ function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode {
> >
<FaviconLogo <FaviconLogo
domain={ domain={
icon.brand === 'claude' icon.domain ??
(icon.brand === 'claude'
? 'claude.ai' ? 'claude.ai'
: icon.brand === 'stripe' : icon.brand === 'stripe'
? 'stripe.com' ? 'stripe.com'
: undefined : undefined)
} }
label={icon.brand} label={icon.brand}
size={16} size={icon.imageSrc ? 14 : 16}
src={icon.imageSrc}
/> />
{icon.overlay === 'link' ? ( {icon.overlay === 'link' ? (
<div <div
@@ -1320,45 +1495,77 @@ function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode {
function SidebarItemComponent({ function SidebarItemComponent({
depth = 0, depth = 0,
interactive = true, interactive = true,
isLastChild = false,
item, item,
onSelect, onSelect,
selectedLabel, selectedLabel,
}: { }: {
depth?: number; depth?: number;
interactive?: boolean; interactive?: boolean;
isLastChild?: boolean;
item: HeroSidebarItem; item: HeroSidebarItem;
onSelect?: (label: string) => void; onSelect?: (label: string) => void;
selectedLabel?: string; selectedLabel?: string;
}) { }) {
const showBranch = depth > 0;
const rowSelectable = interactive && item.href === undefined;
const rowInteractive = rowSelectable || item.href !== undefined;
const rowActive = const rowActive =
interactive && selectedLabel !== undefined && item.label === selectedLabel; rowSelectable && selectedLabel !== undefined && item.label === selectedLabel;
const childItems = item.children ?? [];
const rowContent = (
<>
{showBranch ? <SidebarBranchCell $isLastChild={isLastChild} /> : null}
<SidebarRowMain $withBranch={showBranch}>
{renderSidebarIcon(item.icon)}
<SidebarItemText>
<SidebarItemLabel $active={rowActive}>{item.label}</SidebarItemLabel>
{item.meta ? <SidebarItemMeta>· {item.meta}</SidebarItemMeta> : null}
</SidebarItemText>
</SidebarRowMain>
{item.showChevron || (item.children && item.children.length > 0) ? (
<SidebarChevron>
<ChevronDownMini color={COLORS.textTertiary} size={12} />
</SidebarChevron>
) : null}
</>
);
return ( return (
<> <>
<SidebarItemRow {item.href ? (
$active={rowActive} <SidebarItemRowLink
$depth={depth} $active={rowActive}
onClick={interactive ? () => onSelect?.(item.label) : undefined} $depth={depth}
style={{ cursor: interactive ? 'pointer' : 'default' }} $interactive={rowInteractive}
> $withBranch={showBranch}
{renderSidebarIcon(item.icon)} href={item.href}
<SidebarItemText> rel="noreferrer"
<SidebarItemLabel>{item.label}</SidebarItemLabel> style={{ cursor: rowInteractive ? 'pointer' : 'default' }}
{item.meta ? <SidebarItemMeta>· {item.meta}</SidebarItemMeta> : null} target="_blank"
</SidebarItemText> >
{item.showChevron || (item.children && item.children.length > 0) ? ( {rowContent}
<SidebarChevron> </SidebarItemRowLink>
<ChevronDownMini color={COLORS.textTertiary} size={12} /> ) : (
</SidebarChevron> <SidebarItemRow
) : null} $active={rowActive}
</SidebarItemRow> $depth={depth}
{item.children && item.children.length > 0 ? ( $interactive={rowInteractive}
$withBranch={showBranch}
onClick={rowSelectable ? () => onSelect?.(item.label) : undefined}
style={{ cursor: rowInteractive ? 'pointer' : 'default' }}
>
{rowContent}
</SidebarItemRow>
)}
{childItems.length > 0 ? (
<SidebarChildStack> <SidebarChildStack>
<BranchLine /> <BranchLine />
{item.children.map((child) => ( {childItems.map((child, index) => (
<SidebarItemComponent <SidebarItemComponent
key={child.id} key={child.id}
depth={depth + 1} depth={depth + 1}
isLastChild={index === childItems.length - 1}
interactive={interactive} interactive={interactive}
item={child} item={child}
onSelect={onSelect} onSelect={onSelect}
@@ -1389,7 +1596,7 @@ function PersonTokenCell({
token.kind === 'workflow'; token.kind === 'workflow';
return ( return (
<div style={{ minWidth: 0, position: 'relative', width: '100%' }}> <CellHoverAnchor>
<CellChip <CellChip
clickable={false} clickable={false}
label={token.name} label={token.name}
@@ -1399,11 +1606,7 @@ function PersonTokenCell({
$color={tone.color} $color={tone.color}
$square={square} $square={square}
> >
{token.avatarUrl ? ( <PersonAvatarContent token={token} />
<AvatarImage alt="" src={token.avatarUrl} />
) : (
(token.shortLabel ?? getInitials(token.name))
)}
</PersonAvatarCircle> </PersonAvatarCircle>
} }
/> />
@@ -1413,11 +1616,8 @@ function PersonTokenCell({
<CopyMini /> <CopyMini />
</MiniAction> </MiniAction>
) : null} ) : null}
<MiniAction aria-hidden="true">
<PencilMini />
</MiniAction>
</HoverActions> </HoverActions>
</div> </CellHoverAnchor>
); );
} }
@@ -1468,7 +1668,7 @@ function RelationCellComponent({
hovered: boolean; hovered: boolean;
}) { }) {
return ( return (
<div style={{ minWidth: 0, position: 'relative', width: '100%' }}> <CellHoverAnchor>
<MultiChipStack> <MultiChipStack>
{cell.items.map((item) => { {cell.items.map((item) => {
const tone = PERSON_TONES[item.tone ?? 'gray'] ?? PERSON_TONES.gray; const tone = PERSON_TONES[item.tone ?? 'gray'] ?? PERSON_TONES.gray;
@@ -1494,11 +1694,37 @@ function RelationCellComponent({
<MiniAction aria-hidden="true"> <MiniAction aria-hidden="true">
<CopyMini /> <CopyMini />
</MiniAction> </MiniAction>
<MiniAction aria-hidden="true">
<PencilMini />
</MiniAction>
</HoverActions> </HoverActions>
</div> </CellHoverAnchor>
);
}
function TextCellComponent({
cell,
isFirstColumn,
}: {
cell: HeroCellText;
isFirstColumn: boolean;
}) {
if (!isFirstColumn || !cell.shortLabel) {
return <InlineText>{cell.value}</InlineText>;
}
const tone = PERSON_TONES[cell.tone ?? 'gray'] ?? PERSON_TONES.gray;
return (
<CellChip
clickable={false}
label={cell.value}
leftComponent={
<PersonAvatarCircle
$background={tone.background}
$color={tone.color}
>
{cell.shortLabel}
</PersonAvatarCircle>
}
/>
); );
} }
@@ -1506,10 +1732,13 @@ function renderCellValue(
cell: HeroCellValue, cell: HeroCellValue,
hovered: boolean, hovered: boolean,
isFirstColumn: boolean, isFirstColumn: boolean,
columnId: string,
): ReactNode { ): ReactNode {
const showHoverAction = !ROW_HOVER_ACTION_DISABLED_COLUMNS.has(columnId);
switch (cell.type) { switch (cell.type) {
case 'text': case 'text':
return <InlineText>{cell.value}</InlineText>; return <TextCellComponent cell={cell} isFirstColumn={isFirstColumn} />;
case 'number': case 'number':
return <RightAlignedText>{cell.value}</RightAlignedText>; return <RightAlignedText>{cell.value}</RightAlignedText>;
case 'link': case 'link':
@@ -1520,11 +1749,6 @@ function renderCellValue(
label={cell.value} label={cell.value}
variant={ChipVariant.Static} variant={ChipVariant.Static}
/> />
<HoverActions $visible={hovered}>
<MiniAction aria-hidden="true">
<PencilMini />
</MiniAction>
</HoverActions>
</div> </div>
); );
case 'boolean': case 'boolean':
@@ -1537,17 +1761,24 @@ function renderCellValue(
case 'tag': case 'tag':
return <TagChip>{cell.value}</TagChip>; return <TagChip>{cell.value}</TagChip>;
case 'person': case 'person':
return <PersonTokenCell hovered={hovered} token={cell} />; return (
<PersonTokenCell hovered={hovered && showHoverAction} token={cell} />
);
case 'entity': case 'entity':
return ( return (
<EntityCellComponent <EntityCellComponent
cell={cell} cell={cell}
hovered={hovered} hovered={hovered && showHoverAction}
isFirstColumn={isFirstColumn} isFirstColumn={isFirstColumn}
/> />
); );
case 'relation': case 'relation':
return <RelationCellComponent cell={cell} hovered={hovered} />; return (
<RelationCellComponent
cell={cell}
hovered={hovered && showHoverAction}
/>
);
} }
} }
@@ -1728,11 +1959,10 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
<SidebarTopBar> <SidebarTopBar>
<WorkspaceMenu> <WorkspaceMenu>
<WorkspaceIcon> <WorkspaceIcon>
<IconBrandApple <WorkspaceIconImage
aria-hidden alt=""
color="#ffffff" aria-hidden="true"
size={11} src={APPLE_WORKSPACE_LOGO_SRC}
stroke={2}
/> />
</WorkspaceIcon> </WorkspaceIcon>
<WorkspaceLabel>{visual.workspace.name}</WorkspaceLabel> <WorkspaceLabel>{visual.workspace.name}</WorkspaceLabel>
@@ -1777,7 +2007,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
</SidebarSection> </SidebarSection>
) : null} ) : null}
<SidebarSection> <SidebarSection>
<SidebarSectionLabel>Workspace</SidebarSectionLabel> <SidebarSectionLabel $workspace>Workspace</SidebarSectionLabel>
{visual.workspaceNav.map(renderSidebarEntry)} {visual.workspaceNav.map(renderSidebarEntry)}
</SidebarSection> </SidebarSection>
</SidebarScroll> </SidebarScroll>
@@ -1800,26 +2030,30 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
<NavbarActions aria-hidden> <NavbarActions aria-hidden>
<DesktopOnlyNavbarAction> <DesktopOnlyNavbarAction>
<NavbarDecorativeChip> <NavbarActionButton>
<NavbarDecorativeIconWrap> <NavbarActionIconWrap>
<IconPlus <IconPlus
aria-hidden aria-hidden
size={VISUAL_TOKENS.icon.size.sm} size={VISUAL_TOKENS.icon.size.sm}
stroke={VISUAL_TOKENS.icon.stroke.sm} stroke={VISUAL_TOKENS.icon.stroke.sm}
/> />
</NavbarDecorativeIconWrap> </NavbarActionIconWrap>
New Record <NavbarActionLabel>New Record</NavbarActionLabel>
</NavbarDecorativeChip> </NavbarActionButton>
</DesktopOnlyNavbarAction> </DesktopOnlyNavbarAction>
<NavbarDecorativeChip> <NavbarActionButton>
<NavbarDecorativeIconWrap> <NavbarActionIconWrap>
<IconDotsVertical <IconDotsVertical
aria-hidden aria-hidden
size={VISUAL_TOKENS.icon.size.sm} size={VISUAL_TOKENS.icon.size.sm}
stroke={VISUAL_TOKENS.icon.stroke.sm} stroke={VISUAL_TOKENS.icon.stroke.sm}
/> />
</NavbarDecorativeIconWrap> </NavbarActionIconWrap>
</NavbarDecorativeChip> <NavbarActionSeparator />
<NavbarActionLabel $color={VISUAL_TOKENS.font.color.light}>
K
</NavbarActionLabel>
</NavbarActionButton>
</NavbarActions> </NavbarActions>
</NavbarBar> </NavbarBar>
@@ -1930,6 +2164,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
cell, cell,
hovered, hovered,
!!column.isFirstColumn, !!column.isFirstColumn,
column.id,
) )
: null} : null}
</TableCell> </TableCell>
@@ -25,7 +25,7 @@ const StyledContainer = styled.div<
Pick<ChipProps, 'clickable' | 'isBold' | 'maxWidth' | 'variant'> Pick<ChipProps, 'clickable' | 'isBold' | 'maxWidth' | 'variant'>
>` >`
--chip-horizontal-padding: ${VISUAL_TOKENS.spacing[1]}; --chip-horizontal-padding: ${VISUAL_TOKENS.spacing[1]};
--chip-vertical-padding: ${VISUAL_TOKENS.spacing[1]}; --chip-vertical-padding: 3px;
align-items: center; align-items: center;
background-color: ${({ variant }) => background-color: ${({ variant }) =>
@@ -70,6 +70,9 @@ const StyledContainer = styled.div<
? VISUAL_TOKENS.spacing[2] ? VISUAL_TOKENS.spacing[2]
: 'var(--chip-horizontal-padding)'}; : 'var(--chip-horizontal-padding)'};
user-select: none; user-select: none;
font-family: ${VISUAL_TOKENS.font.family};
font-size: ${VISUAL_TOKENS.font.size.md};
line-height: 1.4;
font-weight: ${({ isBold }) => font-weight: ${({ isBold }) =>
isBold isBold
@@ -83,7 +86,7 @@ const StyledContainer = styled.div<
: variant === ChipVariant.Highlighted : variant === ChipVariant.Highlighted
? VISUAL_TOKENS.background.transparent.medium ? VISUAL_TOKENS.background.transparent.medium
: variant === ChipVariant.Static : variant === ChipVariant.Static
? VISUAL_TOKENS.background.transparent.light ? VISUAL_TOKENS.background.transparent.lighter
: 'inherit'}; : 'inherit'};
} }
@@ -94,7 +97,7 @@ const StyledContainer = styled.div<
: variant === ChipVariant.Highlighted : variant === ChipVariant.Highlighted
? VISUAL_TOKENS.background.transparent.strong ? VISUAL_TOKENS.background.transparent.strong
: variant === ChipVariant.Static : variant === ChipVariant.Static
? VISUAL_TOKENS.background.transparent.light ? VISUAL_TOKENS.background.transparent.lighter
: 'inherit'}; : 'inherit'};
} }
@@ -105,8 +108,10 @@ const StyledContainer = styled.div<
const StyledLabel = styled.span` const StyledLabel = styled.span`
color: inherit; color: inherit;
font-family: inherit;
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
line-height: inherit;
max-width: 100%; max-width: 100%;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -35,8 +35,7 @@ export const VISUAL_TOKENS = {
}, },
}, },
background: { background: {
noisy: noisy: 'url("/images/shared/light-noise.png")',
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.5' numOctaves='6' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.03'/%3E%3C/svg%3E\")",
primary: '#ffffff', primary: '#ffffff',
secondary: '#fcfcfc', secondary: '#fcfcfc',
transparent: { transparent: {
@@ -2,7 +2,12 @@ import type { HeroBaseDataType } from '@/sections/Hero/types/HeroBaseData';
// -- Cell value types -- // -- Cell value types --
export type HeroCellText = { type: 'text'; value: string }; export type HeroCellText = {
type: 'text';
value: string;
shortLabel?: string;
tone?: string;
};
export type HeroCellNumber = { type: 'number'; value: string }; export type HeroCellNumber = { type: 'number'; value: string };
export type HeroCellLink = { type: 'link'; value: string }; export type HeroCellLink = { type: 'link'; value: string };
export type HeroCellBoolean = { type: 'boolean'; value: boolean }; export type HeroCellBoolean = { type: 'boolean'; value: boolean };
@@ -57,7 +62,13 @@ export type HeroRowDef = {
export type HeroSidebarIcon = export type HeroSidebarIcon =
| { kind: 'tabler'; name: string; tone: string; overlay?: 'link' } | { kind: 'tabler'; name: string; tone: string; overlay?: 'link' }
| { kind: 'brand'; brand: string; overlay?: 'link' } | {
kind: 'brand';
brand: string;
domain?: string;
imageSrc?: string;
overlay?: 'link';
}
| { | {
kind: 'avatar'; kind: 'avatar';
label: string; label: string;
@@ -70,6 +81,7 @@ export type HeroSidebarIcon =
export type HeroSidebarItem = { export type HeroSidebarItem = {
id: string; id: string;
label: string; label: string;
href?: string;
icon: HeroSidebarIcon; icon: HeroSidebarIcon;
meta?: string; meta?: string;
active?: boolean; active?: boolean;