mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
04d22feef6
commit
175ae5f0aa
10 changed files with 471 additions and 219 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -52,3 +52,4 @@ mcp.json
|
|||
/.junie/
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
.playwright-cli/
|
||||
|
|
|
|||
Binary file not shown.
|
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 |
BIN
packages/twenty-website-new/public/images/shared/light-noise.png
Normal file
BIN
packages/twenty-website-new/public/images/shared/light-noise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -8,7 +8,7 @@ const PEOPLE_AVATAR_URLS = {
|
|||
jeffWilliams:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-22.png',
|
||||
katherineAdams:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-07.png',
|
||||
'/images/home/hero/avatars/katherine-adams.jpg',
|
||||
philSchiller:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-14.png',
|
||||
timCook:
|
||||
|
|
@ -28,66 +28,6 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
workspace: { icon: 'apple', name: 'Apple' },
|
||||
tableWidth: 1700,
|
||||
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: [
|
||||
{
|
||||
id: 'companies',
|
||||
|
|
@ -369,7 +309,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'people',
|
||||
label: 'People',
|
||||
icon: { kind: 'tabler', name: 'user', tone: 'purple' },
|
||||
icon: { kind: 'tabler', name: 'user', tone: 'blue' },
|
||||
viewLabel: 'All People',
|
||||
viewCount: 5,
|
||||
columns: [
|
||||
|
|
@ -414,7 +354,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
icon: { kind: 'tabler', name: 'targetArrow', tone: 'pink' },
|
||||
icon: { kind: 'tabler', name: 'targetArrow', tone: 'red' },
|
||||
viewLabel: 'All Opportunities',
|
||||
viewCount: 3,
|
||||
columns: [
|
||||
|
|
@ -429,7 +369,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'enterprise-rollout',
|
||||
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' },
|
||||
amount: { type: 'number', value: '$2,300,000' },
|
||||
stage: { type: 'tag', value: 'Negotiation' },
|
||||
|
|
@ -440,7 +385,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'workspace-upgrade',
|
||||
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' },
|
||||
amount: { type: 'number', value: '$750,000' },
|
||||
stage: { type: 'tag', value: 'Qualified' },
|
||||
|
|
@ -467,7 +417,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'send-nda',
|
||||
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 },
|
||||
dueDate: { type: 'text', value: 'Oct 25, 2023' },
|
||||
relatedTo: { type: 'entity', name: 'Anthropic', domain: 'anthropic.com' },
|
||||
|
|
@ -477,7 +432,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'review-proposal',
|
||||
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 },
|
||||
dueDate: { type: 'text', value: 'Oct 28, 2023' },
|
||||
relatedTo: { type: 'entity', name: 'Slack', domain: 'slack.com' },
|
||||
|
|
@ -489,7 +449,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'notes',
|
||||
label: 'Notes',
|
||||
icon: { kind: 'tabler', name: 'notes', tone: 'green' },
|
||||
icon: { kind: 'tabler', name: 'notes', tone: 'teal' },
|
||||
viewLabel: 'All Notes',
|
||||
viewCount: 2,
|
||||
columns: [
|
||||
|
|
@ -502,7 +462,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'discovery-call',
|
||||
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 },
|
||||
relatedTo: { type: 'entity', name: 'Notion', domain: 'notion.com' },
|
||||
added: { type: 'text', value: 'Sep 2, 2023' },
|
||||
|
|
@ -511,7 +476,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'design-system-meeting',
|
||||
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 },
|
||||
relatedTo: { type: 'entity', name: 'Figma', domain: 'figma.com' },
|
||||
added: { type: 'text', value: 'Oct 18, 2023' },
|
||||
|
|
@ -522,7 +492,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'sales-dashboard',
|
||||
label: 'Sales Dashboard',
|
||||
icon: { kind: 'avatar', label: '$', tone: 'amber', shape: 'circle' },
|
||||
icon: { kind: 'avatar', label: 'S', tone: 'amber', shape: 'circle' },
|
||||
meta: 'Dashboard',
|
||||
viewLabel: 'Sales Dashboard',
|
||||
viewCount: 0,
|
||||
|
|
@ -550,7 +520,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'new-lead',
|
||||
cells: {
|
||||
name: { type: 'text', value: 'New Lead Assignment' },
|
||||
name: {
|
||||
type: 'text',
|
||||
value: 'New Lead Assignment',
|
||||
shortLabel: 'N',
|
||||
tone: 'amber',
|
||||
},
|
||||
status: { type: 'tag', value: 'Active' },
|
||||
lastRun: { type: 'text', value: 'Oct 24, 2023 10:00 am' },
|
||||
},
|
||||
|
|
@ -558,7 +533,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'nurture',
|
||||
cells: {
|
||||
name: { type: 'text', value: 'Nurture Sequence' },
|
||||
name: {
|
||||
type: 'text',
|
||||
value: 'Nurture Sequence',
|
||||
shortLabel: 'N',
|
||||
tone: 'amber',
|
||||
},
|
||||
status: { type: 'tag', value: 'Inactive' },
|
||||
lastRun: { type: 'text', value: 'Oct 20, 2023 3:15 pm' },
|
||||
},
|
||||
|
|
@ -582,7 +562,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'run-12345',
|
||||
cells: {
|
||||
runId: { type: 'text', value: 'run_12345' },
|
||||
runId: {
|
||||
type: 'text',
|
||||
value: 'run_12345',
|
||||
shortLabel: 'R',
|
||||
tone: 'amber',
|
||||
},
|
||||
workflow: { type: 'text', value: 'New Lead Assignment' },
|
||||
status: { type: 'tag', value: 'Success' },
|
||||
startedAt: { type: 'text', value: 'Oct 24, 2023 10:00 am' },
|
||||
|
|
@ -592,7 +577,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'run-12346',
|
||||
cells: {
|
||||
runId: { type: 'text', value: 'run_12346' },
|
||||
runId: {
|
||||
type: 'text',
|
||||
value: 'run_12346',
|
||||
shortLabel: 'R',
|
||||
tone: 'amber',
|
||||
},
|
||||
workflow: { type: 'text', value: 'Nurture Sequence' },
|
||||
status: { type: 'tag', value: 'Failed' },
|
||||
startedAt: { type: 'text', value: 'Oct 20, 2023 3:15 pm' },
|
||||
|
|
@ -617,7 +607,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'v2-lead',
|
||||
cells: {
|
||||
version: { type: 'text', value: 'v2' },
|
||||
version: {
|
||||
type: 'text',
|
||||
value: 'v2',
|
||||
shortLabel: 'V',
|
||||
tone: 'amber',
|
||||
},
|
||||
workflow: { type: 'text', value: 'New Lead Assignment' },
|
||||
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' },
|
||||
|
|
@ -626,7 +621,12 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
{
|
||||
id: 'v1-lead',
|
||||
cells: {
|
||||
version: { type: 'text', value: 'v1' },
|
||||
version: {
|
||||
type: 'text',
|
||||
value: 'v1',
|
||||
shortLabel: 'V',
|
||||
tone: 'amber',
|
||||
},
|
||||
workflow: { type: 'text', value: 'New Lead Assignment' },
|
||||
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' },
|
||||
|
|
@ -637,15 +637,15 @@ export const HERO_DATA: HeroHomeDataType = {
|
|||
],
|
||||
},
|
||||
{
|
||||
id: 'claude',
|
||||
label: 'Claude',
|
||||
icon: { kind: 'brand', brand: 'claude', overlay: 'link' },
|
||||
},
|
||||
{
|
||||
id: 'workspace-stripe',
|
||||
label: 'Stripe',
|
||||
icon: { kind: 'brand', brand: 'stripe' },
|
||||
showChevron: true,
|
||||
id: 'book-demo',
|
||||
label: 'Book a demo',
|
||||
href: 'https://cal.com/forms/f7841033-0a20-4958-8c92-4e34ec128a81',
|
||||
icon: {
|
||||
kind: 'brand',
|
||||
brand: 'twenty',
|
||||
imageSrc: '/images/home/hero/twenty-demo-logo.png',
|
||||
overlay: 'link',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { styled } from '@linaria/react';
|
|||
import {
|
||||
IconBook,
|
||||
IconBox,
|
||||
IconBrandApple,
|
||||
IconBrandLinkedin,
|
||||
IconBuildingFactory2,
|
||||
IconBuildingSkyscraper,
|
||||
|
|
@ -52,6 +51,7 @@ import type {
|
|||
HeroCellEntity,
|
||||
HeroCellPerson,
|
||||
HeroCellRelation,
|
||||
HeroCellText,
|
||||
HeroCellValue,
|
||||
HeroColumnDef,
|
||||
HeroRowDef,
|
||||
|
|
@ -66,6 +66,9 @@ import { VISUAL_TOKENS } from './homeVisualTokens';
|
|||
|
||||
const APP_FONT = VISUAL_TOKENS.font.family;
|
||||
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 = {
|
||||
accent: VISUAL_TOKENS.accent.accent9,
|
||||
|
|
@ -88,16 +91,16 @@ const SIDEBAR_TONES: Record<
|
|||
string,
|
||||
{ background: string; border: string; color: string }
|
||||
> = {
|
||||
amber: { background: '#fff4d6', border: '#ffd49b', color: '#9a6700' },
|
||||
blue: { background: '#d9e2fc', border: '#c6d4f9', color: '#3557c6' },
|
||||
gray: { background: '#ebebeb', border: '#d6d6d6', color: '#666666' },
|
||||
amber: { background: '#FEF2A4', border: '#FEF2A4', color: '#35290F' },
|
||||
blue: { background: '#d9e2fc', border: '#c6d4f9', color: '#3A5CCC' },
|
||||
gray: { background: '#ebebeb', border: '#d6d6d6', color: '#838383' },
|
||||
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' },
|
||||
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' },
|
||||
red: { background: '#fee2e2', border: '#fecaca', color: '#b91c1c' },
|
||||
red: { background: '#fdd8d8', border: '#f9c6c6', color: '#DC3D43' },
|
||||
};
|
||||
|
||||
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 NAVIGATION_TABLER_STROKE = 2;
|
||||
const ROW_HOVER_ACTION_DISABLED_COLUMNS = new Set([
|
||||
'createdBy',
|
||||
'accountOwner',
|
||||
]);
|
||||
|
||||
// -- Styled Components --
|
||||
|
||||
|
|
@ -139,16 +146,7 @@ const ShellScene = styled.div`
|
|||
const Frame = styled.div`
|
||||
aspect-ratio: 1280 / 832;
|
||||
background-color: ${COLORS.background};
|
||||
background-image: radial-gradient(
|
||||
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;
|
||||
background-image: ${VISUAL_TOKENS.background.noisy};
|
||||
border: 1px solid ${COLORS.border};
|
||||
border-radius: 8px;
|
||||
box-shadow: ${COLORS.shadow};
|
||||
|
|
@ -157,24 +155,27 @@ const Frame = styled.div`
|
|||
`;
|
||||
|
||||
const AppLayout = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
}
|
||||
`;
|
||||
|
||||
const SidebarPanel = styled.aside`
|
||||
background: transparent;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.04);
|
||||
display: grid;
|
||||
flex: 0 0 72px;
|
||||
gap: 12px;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
min-height: 0;
|
||||
padding: 12px 8px;
|
||||
width: 72px;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
flex-basis: 220px;
|
||||
width: 220px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SidebarTopBar = styled.div`
|
||||
|
|
@ -196,16 +197,17 @@ const WorkspaceMenu = styled.div`
|
|||
|
||||
const WorkspaceIcon = styled.div`
|
||||
align-items: center;
|
||||
background: #111111;
|
||||
border-radius: 2px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
font-family: ${APP_FONT};
|
||||
font-size: 11px;
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
flex: 0 0 auto;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
const WorkspaceIconImage = styled.img`
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const WorkspaceLabel = styled.span`
|
||||
|
|
@ -246,10 +248,17 @@ const SidebarControls = styled.div`
|
|||
display: grid;
|
||||
gap: 8px;
|
||||
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`
|
||||
background: rgba(252, 252, 252, 0.8);
|
||||
background: #fcfcfccc;
|
||||
border: 1px solid ${COLORS.border};
|
||||
border-radius: 40px;
|
||||
display: grid;
|
||||
|
|
@ -261,12 +270,17 @@ const SegmentedRail = styled.div`
|
|||
const Segment = styled.div<{ $selected?: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ $selected }) =>
|
||||
$selected ? 'rgba(0, 0, 0, 0.04)' : 'transparent'};
|
||||
$selected ? '#0000000a' : 'transparent'};
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
height: 22px;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
padding: 0 8px;
|
||||
width: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NewChat = styled.div`
|
||||
|
|
@ -305,8 +319,9 @@ const NewChatLabel = styled.span`
|
|||
`;
|
||||
|
||||
const SidebarScroll = styled.div`
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
|
@ -316,14 +331,15 @@ const SidebarSection = styled.div`
|
|||
gap: 2px;
|
||||
`;
|
||||
|
||||
const SidebarSectionLabel = styled.span`
|
||||
const SidebarSectionLabel = styled.span<{ $workspace?: boolean }>`
|
||||
color: ${COLORS.textLight};
|
||||
display: none;
|
||||
font-family: ${APP_FONT};
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 0 4px 4px;
|
||||
padding: ${({ $workspace }) =>
|
||||
$workspace ? '4px 4px 8px' : '0 4px 4px'};
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
display: block;
|
||||
|
|
@ -333,17 +349,57 @@ const SidebarSectionLabel = styled.span`
|
|||
const SidebarItemRow = styled.div<{
|
||||
$active?: boolean;
|
||||
$depth?: number;
|
||||
$interactive?: boolean;
|
||||
$withBranch?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ $active }) =>
|
||||
$active ? 'rgba(0, 0, 0, 0.04)' : 'transparent'};
|
||||
$active ? VISUAL_TOKENS.background.transparent.medium : 'transparent'};
|
||||
border-radius: 4px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
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 : 24}px`};
|
||||
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 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<{
|
||||
|
|
@ -371,8 +427,8 @@ const SidebarItemText = styled.div`
|
|||
min-width: 0;
|
||||
`;
|
||||
|
||||
const SidebarItemLabel = styled.span`
|
||||
color: ${COLORS.textSecondary};
|
||||
const SidebarItemLabel = styled.span<{ $active?: boolean }>`
|
||||
color: ${({ $active }) => ($active ? COLORS.text : COLORS.textSecondary)};
|
||||
display: none;
|
||||
font-family: ${APP_FONT};
|
||||
font-size: 13px;
|
||||
|
|
@ -423,12 +479,43 @@ const SidebarChildStack = styled.div`
|
|||
const BranchLine = styled.div`
|
||||
background: ${COLORS.borderStrong};
|
||||
bottom: 14px;
|
||||
left: 15px;
|
||||
left: 11px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
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<{
|
||||
$background: string;
|
||||
$color: string;
|
||||
|
|
@ -450,9 +537,11 @@ const SidebarAvatar = styled.div<{
|
|||
`;
|
||||
|
||||
const RightPane = styled.div`
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
grid-template-rows: 32px minmax(0, 1fr);
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: 12px 8px 12px 0;
|
||||
|
||||
|
|
@ -465,6 +554,8 @@ const NavbarBar = styled.div`
|
|||
align-items: center;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex: 0 0 32px;
|
||||
height: 32px;
|
||||
justify-content: space-between;
|
||||
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;
|
||||
background: transparent;
|
||||
border: 1px solid ${VISUAL_TOKENS.background.transparent.medium};
|
||||
border: 1px solid ${NAVBAR_ACTION_BORDER};
|
||||
border-radius: ${VISUAL_TOKENS.border.radius.sm};
|
||||
color: ${VISUAL_TOKENS.font.color.secondary};
|
||||
display: inline-flex;
|
||||
font-family: ${APP_FONT};
|
||||
font-size: ${VISUAL_TOKENS.font.size.md};
|
||||
|
|
@ -540,19 +632,38 @@ const NavbarDecorativeChip = styled.div`
|
|||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const NavbarDecorativeIconWrap = styled.span`
|
||||
const NavbarActionIconWrap = styled.span<{ $color?: string }>`
|
||||
align-items: center;
|
||||
color: currentColor;
|
||||
color: ${({ $color }) => $color ?? VISUAL_TOKENS.font.color.secondary};
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
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`
|
||||
background: ${COLORS.background};
|
||||
border: 1px solid ${COLORS.border};
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-rows: 40px minmax(0, 1fr);
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
|
@ -563,15 +674,19 @@ const ViewbarBar = styled.div`
|
|||
border-bottom: 1px solid ${COLORS.borderLight};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
padding: 8px 8px 8px 12px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ViewSwitcher = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
gap: 4px;
|
||||
height: 24px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
|
|
@ -603,7 +718,11 @@ const TinyDot = styled.div`
|
|||
const ViewActions = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const ViewAction = styled.span`
|
||||
|
|
@ -622,7 +741,11 @@ const ViewAction = styled.span`
|
|||
|
||||
const TableShell = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const GripRail = styled.div`
|
||||
|
|
@ -640,11 +763,14 @@ const GripCell = styled.div`
|
|||
|
||||
const TableViewport = styled.div<{ $dragging: boolean }>`
|
||||
cursor: ${({ $dragging }) => ($dragging ? 'grabbing' : 'grab')};
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: none;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
|
@ -652,6 +778,10 @@ const TableViewport = styled.div<{ $dragging: boolean }>`
|
|||
`;
|
||||
|
||||
const TableCanvas = styled.div<{ $width: number }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
min-width: ${({ $width }) => `${$width}px`};
|
||||
width: ${({ $width }) => `${$width}px`};
|
||||
`;
|
||||
|
|
@ -693,7 +823,7 @@ const TableCell = styled.div<{
|
|||
$align === 'right' ? 'flex-end' : 'flex-start'};
|
||||
left: ${({ $sticky }) => ($sticky ? '0' : 'auto')};
|
||||
min-width: ${({ $width }) => `${$width}px`};
|
||||
padding: 0 8px;
|
||||
padding: 0 ${TABLE_CELL_HORIZONTAL_PADDING}px;
|
||||
position: ${({ $sticky }) => ($sticky ? 'sticky' : 'relative')};
|
||||
z-index: ${({ $header, $sticky }) => {
|
||||
if ($sticky && $header) {
|
||||
|
|
@ -778,6 +908,16 @@ const EntityCellLayout = styled.div`
|
|||
align-items: center;
|
||||
display: flex;
|
||||
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;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -826,7 +966,7 @@ const PersonAvatarCircle = styled.div<{
|
|||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-family: ${APP_FONT};
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
|
|
@ -847,24 +987,29 @@ const BooleanRow = styled.div`
|
|||
gap: 4px;
|
||||
`;
|
||||
|
||||
const HoverActions = styled.div<{ $visible: boolean }>`
|
||||
const HoverActions = styled.div<{ $rightInset?: number; $visible: boolean }>`
|
||||
align-items: center;
|
||||
background: ${COLORS.backgroundSecondary};
|
||||
border: 1px solid ${COLORS.border};
|
||||
background: ${VISUAL_TOKENS.background.transparent.primary};
|
||||
border: 1px solid ${VISUAL_TOKENS.background.transparent.light};
|
||||
border-radius: 4px;
|
||||
bottom: 4px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: ${VISUAL_TOKENS.boxShadow.light};
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
opacity: ${({ $visible }) => ($visible ? 1 : 0)};
|
||||
padding: 2px;
|
||||
padding: 0 4px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
right: ${({ $rightInset = HOVER_ACTION_EDGE_INSET - TABLE_CELL_HORIZONTAL_PADDING }) =>
|
||||
`${$rightInset}px`};
|
||||
top: 4px;
|
||||
transform: translateX(${({ $visible }) => ($visible ? '0' : '4px')});
|
||||
transition:
|
||||
opacity 0.14s ease,
|
||||
transform 0.14s ease;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
const MiniAction = styled.div`
|
||||
|
|
@ -872,9 +1017,9 @@ const MiniAction = styled.div`
|
|||
border-radius: 2px;
|
||||
color: ${COLORS.textSecondary};
|
||||
display: flex;
|
||||
height: 20px;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
const FooterFirstContent = styled.div`
|
||||
|
|
@ -960,6 +1105,7 @@ const HEADER_ICON_MAP: Record<string, typeof IconBuildingSkyscraper> = {
|
|||
|
||||
// -- Utility functions --
|
||||
|
||||
const failedAvatarUrls = new Set<string>();
|
||||
const failedFaviconUrls = new Set<string>();
|
||||
|
||||
function getInitials(value: string) {
|
||||
|
|
@ -1157,15 +1303,17 @@ function CopyMini({ color = COLORS.textSecondary, size = 14 }: MiniIconProps) {
|
|||
// -- Favicon logo component --
|
||||
|
||||
function FaviconLogo({
|
||||
src,
|
||||
domain,
|
||||
label,
|
||||
size = 14,
|
||||
}: {
|
||||
src?: string;
|
||||
domain?: string;
|
||||
label?: string;
|
||||
size?: number;
|
||||
}) {
|
||||
const faviconUrl = getLogoUrlFromDomainName(domain);
|
||||
const faviconUrl = src ?? getLogoUrlFromDomainName(domain);
|
||||
const [localFailedUrl, setLocalFailedUrl] = useState<string | null>(null);
|
||||
const showFavicon =
|
||||
faviconUrl !== undefined &&
|
||||
|
|
@ -1213,6 +1361,31 @@ function FaviconLogo({
|
|||
|
||||
// -- 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 {
|
||||
if (icon.kind === 'brand') {
|
||||
return (
|
||||
|
|
@ -1223,14 +1396,16 @@ function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode {
|
|||
>
|
||||
<FaviconLogo
|
||||
domain={
|
||||
icon.brand === 'claude'
|
||||
icon.domain ??
|
||||
(icon.brand === 'claude'
|
||||
? 'claude.ai'
|
||||
: icon.brand === 'stripe'
|
||||
? 'stripe.com'
|
||||
: undefined
|
||||
: undefined)
|
||||
}
|
||||
label={icon.brand}
|
||||
size={16}
|
||||
size={icon.imageSrc ? 14 : 16}
|
||||
src={icon.imageSrc}
|
||||
/>
|
||||
{icon.overlay === 'link' ? (
|
||||
<div
|
||||
|
|
@ -1320,45 +1495,77 @@ function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode {
|
|||
function SidebarItemComponent({
|
||||
depth = 0,
|
||||
interactive = true,
|
||||
isLastChild = false,
|
||||
item,
|
||||
onSelect,
|
||||
selectedLabel,
|
||||
}: {
|
||||
depth?: number;
|
||||
interactive?: boolean;
|
||||
isLastChild?: boolean;
|
||||
item: HeroSidebarItem;
|
||||
onSelect?: (label: string) => void;
|
||||
selectedLabel?: string;
|
||||
}) {
|
||||
const showBranch = depth > 0;
|
||||
const rowSelectable = interactive && item.href === undefined;
|
||||
const rowInteractive = rowSelectable || item.href !== undefined;
|
||||
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 (
|
||||
<>
|
||||
<SidebarItemRow
|
||||
$active={rowActive}
|
||||
$depth={depth}
|
||||
onClick={interactive ? () => onSelect?.(item.label) : undefined}
|
||||
style={{ cursor: interactive ? 'pointer' : 'default' }}
|
||||
>
|
||||
{renderSidebarIcon(item.icon)}
|
||||
<SidebarItemText>
|
||||
<SidebarItemLabel>{item.label}</SidebarItemLabel>
|
||||
{item.meta ? <SidebarItemMeta>· {item.meta}</SidebarItemMeta> : null}
|
||||
</SidebarItemText>
|
||||
{item.showChevron || (item.children && item.children.length > 0) ? (
|
||||
<SidebarChevron>
|
||||
<ChevronDownMini color={COLORS.textTertiary} size={12} />
|
||||
</SidebarChevron>
|
||||
) : null}
|
||||
</SidebarItemRow>
|
||||
{item.children && item.children.length > 0 ? (
|
||||
{item.href ? (
|
||||
<SidebarItemRowLink
|
||||
$active={rowActive}
|
||||
$depth={depth}
|
||||
$interactive={rowInteractive}
|
||||
$withBranch={showBranch}
|
||||
href={item.href}
|
||||
rel="noreferrer"
|
||||
style={{ cursor: rowInteractive ? 'pointer' : 'default' }}
|
||||
target="_blank"
|
||||
>
|
||||
{rowContent}
|
||||
</SidebarItemRowLink>
|
||||
) : (
|
||||
<SidebarItemRow
|
||||
$active={rowActive}
|
||||
$depth={depth}
|
||||
$interactive={rowInteractive}
|
||||
$withBranch={showBranch}
|
||||
onClick={rowSelectable ? () => onSelect?.(item.label) : undefined}
|
||||
style={{ cursor: rowInteractive ? 'pointer' : 'default' }}
|
||||
>
|
||||
{rowContent}
|
||||
</SidebarItemRow>
|
||||
)}
|
||||
{childItems.length > 0 ? (
|
||||
<SidebarChildStack>
|
||||
<BranchLine />
|
||||
{item.children.map((child) => (
|
||||
{childItems.map((child, index) => (
|
||||
<SidebarItemComponent
|
||||
key={child.id}
|
||||
depth={depth + 1}
|
||||
isLastChild={index === childItems.length - 1}
|
||||
interactive={interactive}
|
||||
item={child}
|
||||
onSelect={onSelect}
|
||||
|
|
@ -1389,7 +1596,7 @@ function PersonTokenCell({
|
|||
token.kind === 'workflow';
|
||||
|
||||
return (
|
||||
<div style={{ minWidth: 0, position: 'relative', width: '100%' }}>
|
||||
<CellHoverAnchor>
|
||||
<CellChip
|
||||
clickable={false}
|
||||
label={token.name}
|
||||
|
|
@ -1399,11 +1606,7 @@ function PersonTokenCell({
|
|||
$color={tone.color}
|
||||
$square={square}
|
||||
>
|
||||
{token.avatarUrl ? (
|
||||
<AvatarImage alt="" src={token.avatarUrl} />
|
||||
) : (
|
||||
(token.shortLabel ?? getInitials(token.name))
|
||||
)}
|
||||
<PersonAvatarContent token={token} />
|
||||
</PersonAvatarCircle>
|
||||
}
|
||||
/>
|
||||
|
|
@ -1413,11 +1616,8 @@ function PersonTokenCell({
|
|||
<CopyMini />
|
||||
</MiniAction>
|
||||
) : null}
|
||||
<MiniAction aria-hidden="true">
|
||||
<PencilMini />
|
||||
</MiniAction>
|
||||
</HoverActions>
|
||||
</div>
|
||||
</CellHoverAnchor>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1468,7 +1668,7 @@ function RelationCellComponent({
|
|||
hovered: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ minWidth: 0, position: 'relative', width: '100%' }}>
|
||||
<CellHoverAnchor>
|
||||
<MultiChipStack>
|
||||
{cell.items.map((item) => {
|
||||
const tone = PERSON_TONES[item.tone ?? 'gray'] ?? PERSON_TONES.gray;
|
||||
|
|
@ -1494,11 +1694,37 @@ function RelationCellComponent({
|
|||
<MiniAction aria-hidden="true">
|
||||
<CopyMini />
|
||||
</MiniAction>
|
||||
<MiniAction aria-hidden="true">
|
||||
<PencilMini />
|
||||
</MiniAction>
|
||||
</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,
|
||||
hovered: boolean,
|
||||
isFirstColumn: boolean,
|
||||
columnId: string,
|
||||
): ReactNode {
|
||||
const showHoverAction = !ROW_HOVER_ACTION_DISABLED_COLUMNS.has(columnId);
|
||||
|
||||
switch (cell.type) {
|
||||
case 'text':
|
||||
return <InlineText>{cell.value}</InlineText>;
|
||||
return <TextCellComponent cell={cell} isFirstColumn={isFirstColumn} />;
|
||||
case 'number':
|
||||
return <RightAlignedText>{cell.value}</RightAlignedText>;
|
||||
case 'link':
|
||||
|
|
@ -1520,11 +1749,6 @@ function renderCellValue(
|
|||
label={cell.value}
|
||||
variant={ChipVariant.Static}
|
||||
/>
|
||||
<HoverActions $visible={hovered}>
|
||||
<MiniAction aria-hidden="true">
|
||||
<PencilMini />
|
||||
</MiniAction>
|
||||
</HoverActions>
|
||||
</div>
|
||||
);
|
||||
case 'boolean':
|
||||
|
|
@ -1537,17 +1761,24 @@ function renderCellValue(
|
|||
case 'tag':
|
||||
return <TagChip>{cell.value}</TagChip>;
|
||||
case 'person':
|
||||
return <PersonTokenCell hovered={hovered} token={cell} />;
|
||||
return (
|
||||
<PersonTokenCell hovered={hovered && showHoverAction} token={cell} />
|
||||
);
|
||||
case 'entity':
|
||||
return (
|
||||
<EntityCellComponent
|
||||
cell={cell}
|
||||
hovered={hovered}
|
||||
hovered={hovered && showHoverAction}
|
||||
isFirstColumn={isFirstColumn}
|
||||
/>
|
||||
);
|
||||
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>
|
||||
<WorkspaceMenu>
|
||||
<WorkspaceIcon>
|
||||
<IconBrandApple
|
||||
aria-hidden
|
||||
color="#ffffff"
|
||||
size={11}
|
||||
stroke={2}
|
||||
<WorkspaceIconImage
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
src={APPLE_WORKSPACE_LOGO_SRC}
|
||||
/>
|
||||
</WorkspaceIcon>
|
||||
<WorkspaceLabel>{visual.workspace.name}</WorkspaceLabel>
|
||||
|
|
@ -1777,7 +2007,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
|
|||
</SidebarSection>
|
||||
) : null}
|
||||
<SidebarSection>
|
||||
<SidebarSectionLabel>Workspace</SidebarSectionLabel>
|
||||
<SidebarSectionLabel $workspace>Workspace</SidebarSectionLabel>
|
||||
{visual.workspaceNav.map(renderSidebarEntry)}
|
||||
</SidebarSection>
|
||||
</SidebarScroll>
|
||||
|
|
@ -1800,26 +2030,30 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
|
|||
|
||||
<NavbarActions aria-hidden>
|
||||
<DesktopOnlyNavbarAction>
|
||||
<NavbarDecorativeChip>
|
||||
<NavbarDecorativeIconWrap>
|
||||
<NavbarActionButton>
|
||||
<NavbarActionIconWrap>
|
||||
<IconPlus
|
||||
aria-hidden
|
||||
size={VISUAL_TOKENS.icon.size.sm}
|
||||
stroke={VISUAL_TOKENS.icon.stroke.sm}
|
||||
/>
|
||||
</NavbarDecorativeIconWrap>
|
||||
New Record
|
||||
</NavbarDecorativeChip>
|
||||
</NavbarActionIconWrap>
|
||||
<NavbarActionLabel>New Record</NavbarActionLabel>
|
||||
</NavbarActionButton>
|
||||
</DesktopOnlyNavbarAction>
|
||||
<NavbarDecorativeChip>
|
||||
<NavbarDecorativeIconWrap>
|
||||
<NavbarActionButton>
|
||||
<NavbarActionIconWrap>
|
||||
<IconDotsVertical
|
||||
aria-hidden
|
||||
size={VISUAL_TOKENS.icon.size.sm}
|
||||
stroke={VISUAL_TOKENS.icon.stroke.sm}
|
||||
/>
|
||||
</NavbarDecorativeIconWrap>
|
||||
</NavbarDecorativeChip>
|
||||
</NavbarActionIconWrap>
|
||||
<NavbarActionSeparator />
|
||||
<NavbarActionLabel $color={VISUAL_TOKENS.font.color.light}>
|
||||
⌘K
|
||||
</NavbarActionLabel>
|
||||
</NavbarActionButton>
|
||||
</NavbarActions>
|
||||
</NavbarBar>
|
||||
|
||||
|
|
@ -1930,6 +2164,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) {
|
|||
cell,
|
||||
hovered,
|
||||
!!column.isFirstColumn,
|
||||
column.id,
|
||||
)
|
||||
: null}
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const StyledContainer = styled.div<
|
|||
Pick<ChipProps, 'clickable' | 'isBold' | 'maxWidth' | 'variant'>
|
||||
>`
|
||||
--chip-horizontal-padding: ${VISUAL_TOKENS.spacing[1]};
|
||||
--chip-vertical-padding: ${VISUAL_TOKENS.spacing[1]};
|
||||
--chip-vertical-padding: 3px;
|
||||
|
||||
align-items: center;
|
||||
background-color: ${({ variant }) =>
|
||||
|
|
@ -70,6 +70,9 @@ const StyledContainer = styled.div<
|
|||
? VISUAL_TOKENS.spacing[2]
|
||||
: 'var(--chip-horizontal-padding)'};
|
||||
user-select: none;
|
||||
font-family: ${VISUAL_TOKENS.font.family};
|
||||
font-size: ${VISUAL_TOKENS.font.size.md};
|
||||
line-height: 1.4;
|
||||
|
||||
font-weight: ${({ isBold }) =>
|
||||
isBold
|
||||
|
|
@ -83,7 +86,7 @@ const StyledContainer = styled.div<
|
|||
: variant === ChipVariant.Highlighted
|
||||
? VISUAL_TOKENS.background.transparent.medium
|
||||
: variant === ChipVariant.Static
|
||||
? VISUAL_TOKENS.background.transparent.light
|
||||
? VISUAL_TOKENS.background.transparent.lighter
|
||||
: 'inherit'};
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +97,7 @@ const StyledContainer = styled.div<
|
|||
: variant === ChipVariant.Highlighted
|
||||
? VISUAL_TOKENS.background.transparent.strong
|
||||
: variant === ChipVariant.Static
|
||||
? VISUAL_TOKENS.background.transparent.light
|
||||
? VISUAL_TOKENS.background.transparent.lighter
|
||||
: 'inherit'};
|
||||
}
|
||||
|
||||
|
|
@ -105,8 +108,10 @@ const StyledContainer = styled.div<
|
|||
|
||||
const StyledLabel = styled.span`
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -35,8 +35,7 @@ export const VISUAL_TOKENS = {
|
|||
},
|
||||
},
|
||||
background: {
|
||||
noisy:
|
||||
"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\")",
|
||||
noisy: 'url("/images/shared/light-noise.png")',
|
||||
primary: '#ffffff',
|
||||
secondary: '#fcfcfc',
|
||||
transparent: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ import type { HeroBaseDataType } from '@/sections/Hero/types/HeroBaseData';
|
|||
|
||||
// -- 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 HeroCellLink = { type: 'link'; value: string };
|
||||
export type HeroCellBoolean = { type: 'boolean'; value: boolean };
|
||||
|
|
@ -57,7 +62,13 @@ export type HeroRowDef = {
|
|||
|
||||
export type HeroSidebarIcon =
|
||||
| { 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';
|
||||
label: string;
|
||||
|
|
@ -70,6 +81,7 @@ export type HeroSidebarIcon =
|
|||
export type HeroSidebarItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
icon: HeroSidebarIcon;
|
||||
meta?: string;
|
||||
active?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue