Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)

Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.

**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).

**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.

**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).

Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Raphaël Bosi
2026-06-18 10:31:29 +02:00
committed by GitHub
parent 640a8e6ca6
commit 8034c7725f
1580 changed files with 2382 additions and 2404 deletions
@@ -0,0 +1,59 @@
.editorLoader {
display: flex;
align-items: center;
justify-content: center;
height: var(--code-editor-height);
border: none;
border-radius: 0;
background-color: var(--t-background-transparent-lighter);
&[data-variant='default'] {
border: 1px solid var(--t-border-color-medium);
border-radius: var(--t-border-radius-sm);
}
&[data-variant='with-header'] {
border: 1px solid var(--t-border-color-medium);
border-top: none;
border-radius: 0 0 var(--t-border-radius-sm) var(--t-border-radius-sm);
}
}
.container {
display: contents;
}
.editorWrapper {
display: contents;
}
.editorWrapper :global(.monaco-editor) {
outline-width: 0;
border-radius: var(--t-border-radius-sm);
background-color: var(--t-background-secondary);
}
.editorWrapper[data-transparent-background] :global(.monaco-editor) {
background-color: transparent;
}
.editorWrapper[data-variant='borderless'] :global(.monaco-editor) {
border-radius: 0;
}
.editorWrapper :global(.overflow-guard) {
box-sizing: border-box;
border: none;
border-radius: 0;
}
.editorWrapper[data-variant='default'] :global(.overflow-guard) {
border: 1px solid var(--t-border-color-medium);
border-radius: var(--t-border-radius-sm);
}
.editorWrapper[data-variant='with-header'] :global(.overflow-guard) {
border: 1px solid var(--t-border-color-medium);
border-top: none;
border-radius: 0 0 var(--t-border-radius-sm) var(--t-border-radius-sm);
}
@@ -0,0 +1,6 @@
declare const classNames: {
readonly editorLoader: 'editorLoader';
readonly container: 'container';
readonly editorWrapper: 'editorWrapper';
};
export default classNames;
@@ -0,0 +1,247 @@
import Editor, { type EditorProps, type Monaco } from '@monaco-editor/react';
import { Loader } from '@ui/feedback/Loader/Loader';
import { BASE_CODE_EDITOR_THEME_ID } from '@ui/input/CodeEditor/constants/BaseCodeEditorThemeId';
import { getBaseCodeEditorTheme } from '@ui/input/CodeEditor/utils/getBaseCodeEditorTheme';
import { ResizeHandle } from '@ui/layout/ResizeHandle/ResizeHandle';
import { useResizeHandle } from '@ui/layout/ResizeHandle/hooks/useResizeHandle';
import { ThemeContext, type ThemeType } from '@ui/theme-constants';
import { type editor } from 'monaco-editor';
import { type KeyboardEvent, useContext, useEffect, useState } from 'react';
import { isDefined } from '@ui/utilities/utils/isDefined';
import styles from './CodeEditor.module.scss';
type CodeEditorVariant = 'default' | 'with-header' | 'borderless';
type CodeEditorContentPadding = 'default' | 'comfortable';
const setCodeEditorTheme = (
monaco: Monaco,
theme: ThemeType,
colorScheme: 'light' | 'dark',
) => {
monaco.editor.defineTheme(
BASE_CODE_EDITOR_THEME_ID,
getBaseCodeEditorTheme(theme, colorScheme),
);
monaco.editor.setTheme(BASE_CODE_EDITOR_THEME_ID);
};
type CodeEditorProps = Pick<
EditorProps,
'value' | 'language' | 'onMount' | 'onValidate' | 'height' | 'options'
> & {
onChange?: (value: string) => void;
setMarkers?: (value: string) => editor.IMarkerData[];
variant?: CodeEditorVariant;
isLoading?: boolean;
transparentBackground?: boolean;
resizable?: boolean;
contentPadding?: CodeEditorContentPadding;
autoHeight?: boolean;
};
export const CodeEditor = ({
value,
language,
onMount,
onChange,
setMarkers,
onValidate,
height = 450,
variant = 'default',
transparentBackground,
isLoading = false,
options,
resizable = false,
contentPadding = 'default',
autoHeight = false,
}: CodeEditorProps) => {
const { theme, colorScheme } = useContext(ThemeContext);
const [monaco, setMonaco] = useState<Monaco | undefined>(undefined);
const [editor, setEditor] = useState<
editor.IStandaloneCodeEditor | undefined
>(undefined);
const [isEditorFocused, setIsEditorFocused] = useState(false);
const [autoHeightContentHeight, setAutoHeightContentHeight] = useState<
number | undefined
>(undefined);
const numericHeight = typeof height === 'number' ? height : 450;
const {
size: resizableHeight,
handleResizeStart,
handleResizeMove,
handleResizeEnd,
} = useResizeHandle({
initialSize: numericHeight,
});
const shouldAutoHeight = autoHeight && !resizable;
const codeEditorPadding =
typeof theme.spacingMultiplicator === 'number'
? theme.spacingMultiplicator * 4
: undefined;
const currentHeight = shouldAutoHeight
? (autoHeightContentHeight ?? height)
: resizable
? resizableHeight
: height;
const contentPaddingOptions = {
...(contentPadding === 'comfortable'
? {
lineDecorationsWidth: codeEditorPadding,
}
: {}),
padding: {
bottom: codeEditorPadding,
top: codeEditorPadding,
},
};
const { padding: _callerPadding, ...editorOptions } = options ?? {};
const setModelMarkers = (
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined,
) => {
const model = editor?.getModel();
if (!isDefined(model)) {
return;
}
const customMarkers = setMarkers?.(model.getValue());
if (isDefined(customMarkers)) {
monaco?.editor.setModelMarkers(model, 'customMarker', customMarkers);
}
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (isEditorFocused) {
event.stopPropagation();
}
};
useEffect(() => {
if (!isDefined(monaco)) {
return;
}
setCodeEditorTheme(monaco, theme, colorScheme);
}, [colorScheme, monaco, theme]);
// Drive the container height from Monaco's content height; the editor's
// default automaticLayout then re-fits the canvas (no manual layout needed).
useEffect(() => {
if (!shouldAutoHeight || !isDefined(editor)) {
setAutoHeightContentHeight(undefined);
return;
}
const updateAutoHeight = () => {
const nextHeight = editor.getContentHeight();
if (!Number.isFinite(nextHeight) || nextHeight <= 0) {
return;
}
setAutoHeightContentHeight((currentHeight) =>
currentHeight === nextHeight ? currentHeight : nextHeight,
);
};
updateAutoHeight();
const disposable = editor.onDidContentSizeChange(updateAutoHeight);
return () => {
disposable.dispose();
};
}, [editor, shouldAutoHeight]);
return isLoading ? (
<div
className={styles.editorLoader}
data-variant={variant}
style={
{
'--code-editor-height':
typeof currentHeight === 'number'
? `${currentHeight}px`
: currentHeight,
} as React.CSSProperties
}
>
<Loader />
</div>
) : (
<div className={styles.container} onKeyDown={handleKeyDown}>
<input
type="hidden"
data-testid="code-editor-value"
value={value ?? ''}
readOnly
/>
<div
className={styles.editorWrapper}
data-variant={variant}
data-transparent-background={transparentBackground || undefined}
>
<Editor
height={currentHeight}
value={value}
language={language}
loading=""
onMount={(editor, monaco) => {
setMonaco(monaco);
setEditor(editor);
setCodeEditorTheme(monaco, theme, colorScheme);
editor.onDidFocusEditorWidget(() => {
setIsEditorFocused(true);
});
editor.onDidBlurEditorWidget(() => {
setIsEditorFocused(false);
});
onMount?.(editor, monaco);
setModelMarkers(editor, monaco);
}}
onChange={(value) => {
if (isDefined(value)) {
onChange?.(value);
setModelMarkers(editor, monaco);
}
}}
onValidate={(markers) => {
onValidate?.(markers);
}}
options={{
formatOnPaste: true,
formatOnType: true,
fontFamily: theme.code.font.family,
bracketPairColorization: {
enabled: false,
},
overviewRulerLanes: 0,
scrollBeyondLastLine: false,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
minimap: {
enabled: false,
},
...editorOptions,
...contentPaddingOptions,
}}
/>
</div>
{resizable && (
<ResizeHandle
onPointerDown={handleResizeStart}
onPointerMove={handleResizeMove}
onPointerUp={handleResizeEnd}
/>
)}
</div>
);
};
@@ -0,0 +1 @@
export const BASE_CODE_EDITOR_THEME_ID = 'baseCodeEditorTheme';
@@ -0,0 +1,143 @@
import { type ThemeType } from '@ui/theme-constants';
import { type editor } from 'monaco-editor';
import { isDefined } from '@ui/utilities/utils/isDefined';
const convertColorToHex = (color: string): string => {
if (color.trim() === 'transparent') {
return '#00000000';
}
const displayP3Match = color.match(
/color\(display-p3\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s+\/\s+([\d.]+))?\)/,
);
if (isDefined(displayP3Match)) {
const [, r, g, b, a] = displayP3Match;
const rHex = Math.round(parseFloat(r) * 255)
.toString(16)
.padStart(2, '0');
const gHex = Math.round(parseFloat(g) * 255)
.toString(16)
.padStart(2, '0');
const bHex = Math.round(parseFloat(b) * 255)
.toString(16)
.padStart(2, '0');
if (isDefined(a)) {
const aHex = Math.round(parseFloat(a) * 255)
.toString(16)
.padStart(2, '0');
return `#${rHex}${gHex}${bHex}${aHex}`;
}
return `#${rHex}${gHex}${bHex}`;
}
return color;
};
export const getBaseCodeEditorTheme = (
theme: ThemeType,
colorScheme: 'light' | 'dark',
): editor.IStandaloneThemeData => {
return {
base: colorScheme === 'dark' ? 'vs-dark' : 'vs',
inherit: true,
rules: [
{
token: '',
foreground: convertColorToHex(theme.font.color.secondary),
},
{
token: 'keyword',
foreground: convertColorToHex(theme.color.pink11),
},
{
token: 'keyword.control',
foreground: convertColorToHex(theme.color.pink11),
},
{
token: 'keyword.json',
foreground: convertColorToHex(theme.color.orange11),
},
{
token: 'number',
foreground: convertColorToHex(theme.color.orange11),
},
{
token: 'number.json',
foreground: convertColorToHex(theme.color.orange11),
},
{
token: 'regexp',
foreground: convertColorToHex(theme.color.orange11),
},
{
token: 'type',
foreground: convertColorToHex(theme.color.green11),
},
{
token: 'attribute.name',
foreground: convertColorToHex(theme.color.blue11),
},
{
token: 'tag',
foreground: convertColorToHex(theme.color.pink11),
},
{
token: 'string',
foreground: convertColorToHex(theme.color.green11),
},
{
token: 'string.key.json',
foreground: convertColorToHex(theme.color.blue11),
},
{
token: 'delimiter',
foreground: convertColorToHex(theme.font.color.light),
},
{
token: 'delimiter.bracket.json',
foreground: convertColorToHex(theme.font.color.light),
},
{
token: 'string.value.json',
foreground: convertColorToHex(theme.color.green11),
},
{
token: 'comment',
foreground: convertColorToHex(theme.font.color.light),
fontStyle: 'italic',
},
],
colors: {
'editor.background': convertColorToHex('transparent'),
'editor.foreground': convertColorToHex(theme.font.color.secondary),
'editorCursor.foreground': convertColorToHex(theme.font.color.primary),
'editorLineNumber.foreground': convertColorToHex(
theme.font.color.extraLight,
),
'editorLineNumber.activeForeground': convertColorToHex(
theme.font.color.light,
),
'editor.lineHighlightBackground': convertColorToHex(
theme.background.tertiary,
),
'editor.selectionBackground': convertColorToHex(
theme.background.transparent.blue,
),
'editor.inactiveSelectionBackground': convertColorToHex(
theme.background.transparent.light,
),
'editorIndentGuide.background1': convertColorToHex(
theme.border.color.light,
),
'editorIndentGuide.activeBackground1': convertColorToHex(
theme.border.color.medium,
),
'editorBracketMatch.background': convertColorToHex(
theme.background.transparent.light,
),
'editorBracketMatch.border': convertColorToHex(theme.border.color.medium),
},
};
};