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:
@@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user