Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)

## Description

Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.

## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
This commit is contained in:
Raphaël Bosi
2026-06-08 18:12:28 +02:00
committed by GitHub
parent a91e737e69
commit c596a5e342
2320 changed files with 6313 additions and 4856 deletions
@@ -1,615 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { JsonTree } from '@ui/json-visualizer/components/JsonTree';
import { isTwoFirstDepths } from '@ui/json-visualizer/utils/isTwoFirstDepths';
import {
expect,
fn,
userEvent,
waitFor,
waitForElementToBeRemoved,
within,
} from 'storybook/test';
const meta: Meta<typeof JsonTree> = {
title: 'UI/JsonVisualizer/JsonTree',
component: JsonTree,
args: {
shouldExpandNodeInitially: () => true,
emptyArrayLabel: 'Empty Array',
emptyObjectLabel: 'Empty Object',
emptyStringLabel: '[empty string]',
arrowButtonCollapsedLabel: 'Expand',
arrowButtonExpandedLabel: 'Collapse',
},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof JsonTree>;
export const String: Story = {
args: {
value: 'Hello',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('Hello');
expect(node).toBeVisible();
},
};
export const StringWithSpecialCharacters: Story = {
args: {
value: 'Merry \n Christmas \t 🎄',
onNodeValueClick: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('Merry Christmas 🎄');
await userEvent.click(node);
await waitFor(() => {
expect(args.onNodeValueClick).toHaveBeenCalledWith(
'Merry \n Christmas \t 🎄',
);
});
},
};
export const Number: Story = {
args: {
value: 42,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('42');
expect(node).toBeVisible();
},
};
export const Boolean: Story = {
args: {
value: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('true');
expect(node).toBeVisible();
},
};
export const Null: Story = {
args: {
value: null,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('null');
expect(node).toBeVisible();
},
};
export const ArraySimple: Story = {
args: {
value: [1, 2, 3],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('3');
expect(node).toBeVisible();
},
};
export const ArrayEmpty: Story = {
args: {
value: [],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emptyState = await canvas.findByText('Empty Array');
expect(emptyState).toBeVisible();
},
};
export const ArrayNested: Story = {
args: {
value: [1, 2, ['a', 'b', 'c'], 3],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedArrayElements = await canvas.findByText('[3]');
expect(nestedArrayElements).toBeVisible();
},
};
export const ArrayNestedEmpty: Story = {
args: {
value: [1, 2, [], 3],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedArrayElements = await canvas.findByText('[0]');
expect(nestedArrayElements).toBeVisible();
const emptyState = await canvas.findByText('Empty Array');
expect(emptyState).toBeVisible();
},
};
export const ArrayWithObjects: Story = {
args: {
value: [
{
name: 'John Doe',
age: 30,
},
{
name: 'John Dowl',
age: 42,
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
expect(nestedObjectItemsCounts).toHaveLength(2);
},
};
export const ObjectSimple: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const name = await canvas.findByText('John Doe');
expect(name).toBeVisible();
const age = await canvas.findByText('30');
expect(age).toBeVisible();
},
};
export const ObjectEmpty: Story = {
args: {
value: {},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emptyState = await canvas.findByText('Empty Object');
expect(emptyState).toBeVisible();
},
};
export const ObjectNested: Story = {
args: {
value: {
person: {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'New York',
},
},
isActive: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
expect(nestedObjectItemsCounts).toHaveLength(2);
},
};
export const ObjectNestedEmpty: Story = {
args: {
value: {
person: {},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedObjectItemsCount = await canvas.findByText('{0}');
expect(nestedObjectItemsCount).toBeVisible();
const emptyState = await canvas.findByText('Empty Object');
expect(emptyState).toBeVisible();
},
};
export const ObjectWithArray: Story = {
args: {
value: {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
],
settings: {
theme: 'dark',
notifications: true,
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedArrayCount = await canvas.findByText('[2]');
expect(nestedArrayCount).toBeVisible();
const nestedObjectCounts = await canvas.findAllByText('{2}');
expect(nestedObjectCounts).toHaveLength(3);
},
};
export const NestedElementCanBeCollapsed: Story = {
args: {
value: {
person: {
name: 'John Doe',
age: 12,
},
isActive: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const toggleButton = await canvas.findByRole('button', {
name: 'Collapse',
});
const ageElement = canvas.getByText('age');
await Promise.all([
waitForElementToBeRemoved(ageElement),
userEvent.click(toggleButton),
]);
expect(toggleButton).toHaveTextContent('Expand');
},
};
export const ExpandingElementExpandsAllItsDescendants: Story = {
args: {
value: {
person: {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'New York',
country: {
name: 'USA',
code: 'US',
},
},
},
isActive: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
{
const allCollapseButtons = await canvas.findAllByRole('button', {
name: 'Collapse',
});
expect(allCollapseButtons).toHaveLength(3);
for (const collapseButton of allCollapseButtons.reverse()) {
await userEvent.click(collapseButton);
}
}
const rootExpandButton = await canvas.findByRole('button', {
name: 'Expand',
});
await userEvent.click(rootExpandButton);
{
const allCollapseButtons = await canvas.findAllByRole('button', {
name: 'Collapse',
});
expect(allCollapseButtons).toHaveLength(3);
}
},
};
export const ExpandTwoFirstDepths: Story = {
args: {
value: {
person: {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'New York',
country: {
name: 'USA',
code: 'US',
},
},
},
isActive: true,
},
shouldExpandNodeInitially: isTwoFirstDepths,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nameElement = await canvas.findByText('name');
expect(nameElement).toBeVisible();
const addressElement = await canvas.findByText('address');
expect(addressElement).toBeVisible();
const streetElement = canvas.queryByText('street');
expect(streetElement).not.toBeInTheDocument();
const countrCodeElement = canvas.queryByText('code');
expect(countrCodeElement).not.toBeInTheDocument();
},
};
export const ReallyDeepNestedObject: Story = {
args: {
value: {
a: {
b: {
c: {
d: {
e: {
f: {
g: {
h: {
i: {
j: {
k: {
l: {
m: {
n: {
o: {
p: {
q: {
r: {
s: {
t: {
u: {
v: {
w: {
x: {
y: {
z: {
end: true,
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
bis: {
c: {
d: {
e: {
f: {
g: {
h: {
i: {
j: {
k: {
l: {
m: {
n: {
o: {
p: {
q: {
r: {
s: {
t: {
u: {
v: {
w: {
x: {
y: {
z: {
end: true,
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const finalNodes = await canvas.findAllByText('end');
expect(finalNodes).toHaveLength(2);
expect(finalNodes[0]).toBeVisible();
expect(finalNodes[1]).toBeVisible();
},
};
export const LongText: Story = {
args: {
value: {
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.':
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const label = await canvas.findByText(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.',
);
expect(label).toBeVisible();
const value = await canvas.findByText(
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
);
expect(value).toBeVisible();
},
};
export const BlueHighlighting: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
},
getNodeHighlighting: () => 'blue',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ageElement = await canvas.findByText('age');
expect(ageElement).toBeVisible();
},
};
export const PartialBlueHighlighting: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
address: {
city: 'Paris',
},
},
getNodeHighlighting: (keyPath: string) =>
keyPath === 'address' ? 'partial-blue' : undefined,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ageElement = await canvas.findByText('age');
expect(ageElement).toBeVisible();
},
};
export const RedHighlighting: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
address: {
city: 'Paris',
},
},
getNodeHighlighting: () => 'red',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ageElement = await canvas.findByText('age');
expect(ageElement).toBeVisible();
},
};
export const CopyJsonNodeValue: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
},
onNodeValueClick: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const nameValue = await canvas.findByText('John Doe');
await userEvent.click(nameValue);
await waitFor(() => {
expect(args.onNodeValueClick).toHaveBeenCalledWith('John Doe');
});
const ageValue = await canvas.findByText('30');
await userEvent.click(ageValue);
await waitFor(() => {
expect(args.onNodeValueClick).toHaveBeenCalledWith('30');
});
},
};
@@ -1,38 +0,0 @@
import { IconBrackets } from '@ui/display';
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { type JsonArray } from 'type-fest';
export const JsonArrayNode = ({
label,
value,
depth,
keyPath,
highlighting,
}: {
label?: string;
value: JsonArray;
depth: number;
keyPath: string;
highlighting: JsonNodeHighlighting | undefined;
}) => {
const { emptyArrayLabel } = useJsonTreeContextOrThrow();
return (
<JsonNestedNode
elements={[...value.entries()].map(([key, value]) => ({
id: key,
label: String(key),
value,
}))}
renderElementsCount={(count) => `[${count}]`}
label={label}
Icon={IconBrackets}
depth={depth}
emptyElementsText={emptyArrayLabel}
keyPath={keyPath}
highlighting={highlighting}
/>
);
};
@@ -1,157 +0,0 @@
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import { type IconComponent } from '@ui/display';
import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow';
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { themeCssVariables } from '@ui/theme-constants';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type JsonValue } from 'type-fest';
const StyledContainer = styled.li`
display: grid;
list-style-type: none;
`;
const StyledLabelContainer = styled.div`
display: flex;
align-items: center;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledElementsCount = styled.span<{
variant?: 'red';
}>`
color: ${({ variant }) =>
variant === 'red'
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.tertiary};
`;
const StyledJsonListBase = styled.ul<{
depth: number;
}>`
margin: 0;
padding: 0;
display: grid;
row-gap: ${themeCssVariables.spacing[2]};
padding-left: ${({ depth }) =>
depth > 0 ? themeCssVariables.spacing[8] : '0'};
> :first-of-type {
margin-top: ${({ depth }) =>
depth > 0 ? themeCssVariables.spacing[2] : '0'};
}
`;
export const JsonNestedNode = ({
label,
Icon,
elements,
renderElementsCount,
emptyElementsText,
depth,
keyPath,
highlighting,
}: {
label?: string;
Icon: IconComponent;
elements: Array<{ id: string | number; label: string; value: JsonValue }>;
renderElementsCount?: (count: number) => string;
emptyElementsText: string;
depth: number;
keyPath: string;
highlighting?: JsonNodeHighlighting | undefined;
}) => {
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
const hideRoot = !isDefined(label);
const [isOpen, setIsOpen] = useState(
shouldExpandNodeInitially({ keyPath, depth }),
);
const renderedChildren = (
<motion.div
initial={{ height: 0, opacity: 0, overflow: 'clip' }}
animate={{ height: 'auto', opacity: 1, overflow: 'clip' }}
exit={{ height: 0, opacity: 0, overflow: 'clip' }}
transition={{ duration: 0.3 }}
>
<StyledJsonListBase depth={depth}>
{elements.length === 0 ? (
<JsonNodeValue valueAsString={emptyElementsText} />
) : (
elements.map(({ id, label, value }) => {
const nextKeyPath = isNonEmptyString(keyPath)
? `${keyPath}.${id}`
: String(id);
return (
<JsonNode
key={id}
label={label}
value={value}
depth={depth + 1}
keyPath={nextKeyPath}
/>
);
})
)}
</StyledJsonListBase>
</motion.div>
);
const handleArrowClick = () => {
setIsOpen(!isOpen);
};
if (hideRoot) {
return (
<StyledContainer>
<AnimatePresence initial={false}>{renderedChildren}</AnimatePresence>
</StyledContainer>
);
}
return (
<StyledContainer>
<StyledLabelContainer>
<JsonArrow
isOpen={isOpen}
onClick={handleArrowClick}
variant={
highlighting === 'partial-blue'
? 'blue'
: highlighting === 'red'
? highlighting
: undefined
}
/>
<JsonNodeLabel
label={label}
Icon={Icon}
highlighting={highlighting === 'red' ? highlighting : undefined}
/>
{renderElementsCount && (
<StyledElementsCount
variant={highlighting === 'red' ? 'red' : undefined}
>
{renderElementsCount(elements.length)}
</StyledElementsCount>
)}
</StyledLabelContainer>
<AnimatePresence initial={false}>
{isOpen && renderedChildren}
</AnimatePresence>
</StyledContainer>
);
};
@@ -1,101 +0,0 @@
import {
isBoolean,
isNonEmptyString,
isNumber,
isString,
} from '@sniptt/guards';
import {
IconCheckbox,
IconCircleOff,
IconNumber9,
IconTypography,
} from '@ui/display';
import { JsonArrayNode } from '@ui/json-visualizer/components/JsonArrayNode';
import { JsonObjectNode } from '@ui/json-visualizer/components/JsonObjectNode';
import { JsonValueNode } from '@ui/json-visualizer/components/JsonValueNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { isArray } from '@ui/json-visualizer/utils/isArray';
import { isDefined } from 'twenty-shared/utils';
import { type JsonValue } from 'type-fest';
export const JsonNode = ({
label,
value,
depth,
keyPath,
}: {
label?: string;
value: JsonValue;
depth: number;
keyPath: string;
}) => {
const { getNodeHighlighting, emptyStringLabel } = useJsonTreeContextOrThrow();
const highlighting = getNodeHighlighting?.(keyPath);
if (!isDefined(value)) {
return (
<JsonValueNode
label={label}
valueAsString="null"
Icon={IconCircleOff}
highlighting={highlighting}
/>
);
}
if (isString(value)) {
return (
<JsonValueNode
label={label}
valueAsString={isNonEmptyString(value) ? value : emptyStringLabel}
Icon={IconTypography}
highlighting={highlighting}
/>
);
}
if (isNumber(value)) {
return (
<JsonValueNode
label={label}
valueAsString={String(value)}
Icon={IconNumber9}
highlighting={highlighting}
/>
);
}
if (isBoolean(value)) {
return (
<JsonValueNode
label={label}
valueAsString={String(value)}
Icon={IconCheckbox}
highlighting={highlighting}
/>
);
}
if (isArray(value)) {
return (
<JsonArrayNode
label={label}
value={value}
depth={depth}
keyPath={keyPath}
highlighting={highlighting}
/>
);
}
return (
<JsonObjectNode
label={label}
value={value}
depth={depth}
keyPath={keyPath}
highlighting={highlighting}
/>
);
};
@@ -1,38 +0,0 @@
import { IconCube } from '@ui/display';
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { type JsonObject } from 'type-fest';
export const JsonObjectNode = ({
label,
value,
depth,
keyPath,
highlighting,
}: {
label?: string;
value: JsonObject;
depth: number;
keyPath: string;
highlighting: JsonNodeHighlighting | undefined;
}) => {
const { emptyObjectLabel } = useJsonTreeContextOrThrow();
return (
<JsonNestedNode
elements={Object.entries(value).map(([key, value]) => ({
id: key,
label: key,
value,
}))}
renderElementsCount={(count) => `{${count}}`}
label={label}
Icon={IconCube}
depth={depth}
emptyElementsText={emptyObjectLabel}
keyPath={keyPath}
highlighting={highlighting}
/>
);
};
@@ -1,49 +0,0 @@
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { JsonTreeContextProvider } from '@ui/json-visualizer/components/JsonTreeContextProvider';
import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
import { type JsonValue } from 'type-fest';
export const JsonTree = ({
value,
getNodeHighlighting,
shouldExpandNodeInitially,
emptyArrayLabel,
emptyObjectLabel,
emptyStringLabel,
arrowButtonCollapsedLabel,
arrowButtonExpandedLabel,
onNodeValueClick,
}: {
value: JsonValue;
getNodeHighlighting?: GetJsonNodeHighlighting;
shouldExpandNodeInitially: (
params: ShouldExpandNodeInitiallyProps,
) => boolean;
emptyArrayLabel: string;
emptyObjectLabel: string;
emptyStringLabel: string;
arrowButtonCollapsedLabel: string;
arrowButtonExpandedLabel: string;
onNodeValueClick?: (valueAsString: string) => void;
}) => {
return (
<JsonTreeContextProvider
value={{
getNodeHighlighting,
shouldExpandNodeInitially,
emptyArrayLabel,
emptyObjectLabel,
emptyStringLabel,
arrowButtonCollapsedLabel,
arrowButtonExpandedLabel,
onNodeValueClick,
}}
>
<JsonList depth={0}>
<JsonNode value={value} depth={0} keyPath="" />
</JsonList>
</JsonTreeContextProvider>
);
};
@@ -1,18 +0,0 @@
import {
JsonTreeContext,
type JsonTreeContextType,
} from '@ui/json-visualizer/contexts/JsonTreeContext';
export const JsonTreeContextProvider = ({
value,
children,
}: {
value: JsonTreeContextType;
children: React.ReactNode;
}) => {
return (
<JsonTreeContext.Provider value={value}>
{children}
</JsonTreeContext.Provider>
);
};
@@ -1,47 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { themeCssVariables } from '@ui/theme-constants';
const StyledListItem = styled.li`
align-items: center;
column-gap: ${themeCssVariables.spacing[2]};
display: flex;
list-style-type: none;
white-space: nowrap;
`;
type JsonValueNodeProps = {
valueAsString: string;
highlighting: JsonNodeHighlighting | undefined;
} & (
| {
label: string;
Icon: IconComponent;
}
| {
label?: never;
Icon?: unknown;
}
);
export const JsonValueNode = (props: JsonValueNodeProps) => {
return (
<StyledListItem>
{props.label && (
<JsonNodeLabel
label={props.label}
Icon={props.Icon}
highlighting={props.highlighting}
/>
)}
<JsonNodeValue
valueAsString={props.valueAsString}
highlighting={props.highlighting}
/>
</StyledListItem>
);
};
@@ -1,68 +0,0 @@
import { styled } from '@linaria/react';
import { VisibilityHidden } from '@ui/accessibility';
import { IconChevronDown } from '@ui/display';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { useContext } from 'react';
import { motion } from 'framer-motion';
const StyledButton = styled.button<{
variant?: 'blue' | 'red';
}>`
align-items: center;
background-color: ${({ variant }) =>
variant === 'red'
? themeCssVariables.background.danger
: themeCssVariables.background.transparent.lighter};
border-color: ${({ variant }) =>
variant === 'red'
? themeCssVariables.border.color.danger
: themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
border-style: solid;
border-width: 1px;
display: flex;
justify-content: center;
padding-inline: ${themeCssVariables.spacing[1]};
height: 24px;
width: 24px;
box-sizing: border-box;
cursor: pointer;
`;
export const JsonArrow = ({
isOpen,
onClick,
variant,
}: {
isOpen: boolean;
onClick: () => void;
variant?: 'blue' | 'red';
}) => {
const { theme } = useContext(ThemeContext);
const { arrowButtonCollapsedLabel, arrowButtonExpandedLabel } =
useJsonTreeContextOrThrow();
const iconColor =
variant === 'blue'
? themeCssVariables.color.blue
: variant === 'red'
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.secondary;
return (
<StyledButton variant={variant} onClick={onClick}>
<VisibilityHidden>
{isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel}
</VisibilityHidden>
<motion.div
initial={false}
animate={{ rotate: isOpen ? 0 : -90 }}
transition={{ duration: 0.3 }}
>
<IconChevronDown size={theme.icon.size.md} color={iconColor} />
</motion.div>
</StyledButton>
);
};
@@ -1,23 +0,0 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from '@ui/theme-constants';
const StyledList = styled.ul<{ depth: number }>`
margin: 0;
padding: 0;
display: grid;
row-gap: ${themeCssVariables.spacing[2]};
${({ depth }) =>
depth > 0
? `
padding-left: ${themeCssVariables.spacing[8]};
> :first-of-type {
margin-top: ${themeCssVariables.spacing[2]};
}
`
: ''}
`;
export { StyledList as JsonList };
@@ -1,10 +0,0 @@
import { styled } from '@linaria/react';
const StyledListItem = styled.li`
align-items: center;
display: flex;
list-style-type: none;
white-space: nowrap;
`;
export { StyledListItem as JsonListItem };
@@ -1,74 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { useContext } from 'react';
const StyledLabelContainer = styled.span<{
highlighting?: JsonNodeHighlighting;
}>`
background-color: ${({ highlighting }) =>
highlighting === 'blue'
? themeCssVariables.color.blue3
: highlighting === 'red'
? themeCssVariables.background.danger
: themeCssVariables.background.transparent.lighter};
border-color: ${({ highlighting }) =>
highlighting === 'blue'
? themeCssVariables.color.blue5
: highlighting === 'red'
? themeCssVariables.border.color.danger
: themeCssVariables.border.color.medium};
color: ${({ highlighting }) =>
highlighting === 'blue'
? themeCssVariables.color.blue
: highlighting === 'red'
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.primary};
border-radius: ${themeCssVariables.border.radius.sm};
border-style: solid;
border-width: 1px;
column-gap: ${themeCssVariables.spacing[2]};
display: inline-flex;
align-items: center;
height: 24px;
box-sizing: border-box;
font-size: ${themeCssVariables.font.size.md};
white-space: nowrap;
padding-inline: ${themeCssVariables.spacing[2]};
> span {
align-items: center;
display: inline-flex;
line-height: 1;
}
`;
export const JsonNodeLabel = ({
label,
Icon,
highlighting,
}: {
label: string;
Icon: IconComponent;
highlighting?: JsonNodeHighlighting | undefined;
}) => {
const { theme } = useContext(ThemeContext);
return (
<StyledLabelContainer highlighting={highlighting}>
<Icon
size={theme.icon.size.md}
color={
highlighting === 'blue'
? theme.color.blue
: highlighting === 'red'
? theme.font.color.danger
: theme.font.color.tertiary
}
/>
<span>{label}</span>
</StyledLabelContainer>
);
};
@@ -1,40 +0,0 @@
import { styled } from '@linaria/react';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { themeCssVariables } from '@ui/theme-constants';
const StyledText = styled.span<{
highlighting: JsonNodeHighlighting | undefined;
}>`
align-items: center;
box-sizing: border-box;
color: ${({ highlighting }) =>
highlighting === 'blue'
? themeCssVariables.color.blue8
: highlighting === 'red'
? themeCssVariables.color.red8
: themeCssVariables.font.color.tertiary};
display: inline-flex;
height: 24px;
line-height: 1;
`;
export const JsonNodeValue = ({
valueAsString,
highlighting,
}: {
valueAsString: string;
highlighting?: JsonNodeHighlighting | undefined;
}) => {
const { onNodeValueClick } = useJsonTreeContextOrThrow();
const handleClick = () => {
onNodeValueClick?.(valueAsString);
};
return (
<StyledText highlighting={highlighting} onClick={handleClick}>
{valueAsString}
</StyledText>
);
};
@@ -1,21 +0,0 @@
import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
import { createContext } from 'react';
export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number };
export type JsonTreeContextType = {
getNodeHighlighting?: GetJsonNodeHighlighting;
shouldExpandNodeInitially: (
params: ShouldExpandNodeInitiallyProps,
) => boolean;
emptyStringLabel: string;
emptyArrayLabel: string;
emptyObjectLabel: string;
arrowButtonCollapsedLabel: string;
arrowButtonExpandedLabel: string;
onNodeValueClick?: (valueAsString: string) => void;
};
export const JsonTreeContext = createContext<JsonTreeContextType | undefined>(
undefined,
);
@@ -1,15 +0,0 @@
import { JsonTreeContext } from '@ui/json-visualizer/contexts/JsonTreeContext';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useJsonTreeContextOrThrow = () => {
const value = useContext(JsonTreeContext);
if (!isDefined(value)) {
throw new Error(
'useJsonTreeContextOrThrow must be used within a JsonTreeContextProvider',
);
}
return value;
};
@@ -7,20 +7,4 @@
* |___/
*/
export { JsonArrayNode } from './components/JsonArrayNode';
export { JsonNestedNode } from './components/JsonNestedNode';
export { JsonNode } from './components/JsonNode';
export { JsonObjectNode } from './components/JsonObjectNode';
export { JsonTree } from './components/JsonTree';
export { JsonTreeContextProvider } from './components/JsonTreeContextProvider';
export { JsonValueNode } from './components/JsonValueNode';
export type {
ShouldExpandNodeInitiallyProps,
JsonTreeContextType,
} from './contexts/JsonTreeContext';
export { JsonTreeContext } from './contexts/JsonTreeContext';
export { useJsonTreeContextOrThrow } from './hooks/useJsonTreeContextOrThrow';
export type { GetJsonNodeHighlighting } from './types/GetJsonNodeHighlighting';
export type { JsonNodeHighlighting } from './types/JsonNodeHighlighting';
export { isArray } from './utils/isArray';
export { isTwoFirstDepths } from './utils/isTwoFirstDepths';
export {};
@@ -1,5 +0,0 @@
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
export type GetJsonNodeHighlighting = (
keyPath: string,
) => JsonNodeHighlighting | undefined;
@@ -1,5 +0,0 @@
import { type ThemeColor } from '@ui/theme';
export type JsonNodeHighlighting =
| Extract<ThemeColor, 'blue' | 'red'>
| 'partial-blue';
@@ -1,5 +0,0 @@
import { isArray as _isArray } from '@sniptt/guards';
export const isArray = (
value: unknown,
): value is unknown[] | readonly unknown[] => _isArray(value);
@@ -1,4 +0,0 @@
import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
export const isTwoFirstDepths = ({ depth }: ShouldExpandNodeInitiallyProps) =>
depth <= 1;