TDD: twenty-ui render coverage in the front-component sandbox — golden tests cataloging every sandbox gap (fully green) (#23203)
## What — TDD coverage layer, no fixes Renders every twenty-ui component export inside the real front-component sandbox (opaque-origin iframe + remote-dom worker, real SDK esbuild pipeline) through the existing Playwright-backed storybook vitest suite, and expresses **every sandbox gap as a golden known-failure test**. This PR deliberately ships **zero fixes** and is **fully green (267/267)**: each known issue has a test asserting the exact current broken behavior. A golden test fails on regression (an unexpected component starts failing) AND on fix (the documented failure disappears) — so every future fix must flip its golden assertion to the strict one, whose acceptance criteria are spelled out in each story's comment. - One gallery fixture per submodule under `src/__stories__/twenty-ui-gallery/` (~150 components) - `component-gallery.tsx`: each component renders inside its own error boundary; failures are aggregated with component name and error message (`data-failed-messages`) so one crash cannot mask the rest - Behavior-level stories that a fake fix cannot pass (MutationObserver must actually fire; a link click must reach `hostApi.navigate`) - `vitest.config.ts` re-export so the storybook in-UI "Run tests" button works ## Current state: 267/267 green — mergeable; 18 golden tests encode the catalog below ## Failure catalog (each entry = golden tests asserting today's broken behavior) ### 1. `MutationObserver.observe is not a function` — 6 failing tests `@remote-dom/polyfill` ships `MutationObserver` as an empty class: construction succeeds, the first `.observe()` call crashes. | Failing test | twenty-ui export(s) | Call site | |---|---|---| | Input React + Preact | `Radio`, `RadioGroup`, `CardPicker` | `@base-ui/react` field internals observe form state | | Surfaces React + Preact | `AppTooltip` | twenty-ui observes `document.body` to track anchors | | Worker Platform APIs: Mutation Observer React + Preact | (behavior test) | asserts the observer actually fires on a React-driven insertion — a no-op stub cannot pass it | Validated fix (branch history, commit 94b96d01e79e): implement real mutation semantics **locally in the worker** by tapping the same `@remote-dom/polyfill` hooks (`insertChild`/`removeChild`/`setAttribute`/`removeAttribute`/`setText`) remote-dom already uses to mirror mutations to the host. The worker tree is the source of truth — no host bridge needed. ### 2. `getComputedStyle is not a function` — 4 failing tests The remote-dom `Window` polyfill does not implement `getComputedStyle`; `@base-ui/react` Collapsible calls it on open. | Failing test | twenty-ui export(s) | |---|---| | Layout React + Preact | `AnimatedEaseInOut`, `AnimatedExpandableContainer` | | Json Visualizer React + Preact | `JsonTree`, `JsonArrayNode`, `JsonObjectNode`, `JsonNestedNode` (all via `JsonNestedNode`'s Collapsible) | Validated fix (commit 7af577054fd9): `getComputedStyle` is synchronous and layout only exists host-side, so a bridge is impossible — an inert-but-valid stub (`0s` durations, `none` animation names) is the honest terminal state. ### 3. No router context in the sandbox — 5 failing tests react-router's `Link` reads `NavigationContext`, which has no provider inside the worker: `Cannot destructure property 'basename' of 'useContext(...)' as it is null`. | Failing test | twenty-ui export(s) | |---|---| | Navigation React + Preact | `RawLink`, `UndecoratedLink` | | Data Display React + Preact | `LinkChip` | | HostApi: Router Link | acceptance test — the link must render AND its click must reach `hostApi.navigate` | Validated fix (commit 221c9c353dc8): SDK build plugin wraps the component tree in a low-level react-router `<Router>` whose custom navigator forwards `push`/`replace` to the SDK `navigate` host API (NOT a MemoryRouter, which would render links but swallow clicks into in-memory history). Falls back to a pass-through provider when the app has no react-router-dom dependency. **Security finding bundled in that commit:** clicking a mirrored `<a>` performs a native host-page navigation before the async worker round-trip can `preventDefault` — a front component can escape the sandbox by rendering a link. Fix: apply the renderer's existing preventDefault-then-forward guard (already used for form submits) to anchor clicks, keeping `target="_blank"` native. The Router Link story's flip-to acceptance assertions (click must reach `hostApi.navigate`) will hard-crash the vitest browser the moment links render without this guard — by design. ### 4. Open `Modal` hangs the React runtime — 1 failing test base-ui Dialog portal with `isOpen` never commits under the React runtime (no error thrown); works under Preact. | Failing test | twenty-ui export | |---|---| | Modal Open React (Modal Open Preact passes) | `Modal` | Isolated in its own fixture so the hang cannot mask the rest of the surfaces gallery (which keeps a closed Modal for mount coverage). Root cause not yet identified — first experiment: portal into a worker-owned container instead of the polyfilled `document.body`. ### 5. monaco cannot load in the sandbox — 2 failing tests `@monaco-editor/react` lazy-loads monaco via script injection, impossible in the polyfilled worker DOM (opaque-origin CSP, no script loading). The CodeEditor wrapper mounts; monaco's `onMount` never fires. | Failing test | twenty-ui export | |---|---| | Code Editor React + Preact | `CodeEditor` | Probably wont-fix in the worker: if front components need a code editor, the path is a host-rendered privileged component. The failing test is the documentation. ### Also worth knowing (no failing test possible yet) - `ResizeObserver` / `IntersectionObserver` / `matchMedia` are absent in the worker: components mount without them today only because nothing crashes at mount — behavior like popup auto-resize and `useIsMobile` is silently wrong. Real fix is a host bridge (async observers on the component's own mirrored elements only — never the host document). Behavior tests should land with that fix. - `Icon`/`IconsProvider`/`useIcons` are excluded from galleries by choice: they dynamic-import the multi-MB Tabler catalog; direct icon imports are the supported pattern in bundled front components. ## Iteration plan Each fix is one commit + one set of stories flipping green, in suggested order: 1. Worker-local MutationObserver (6 tests) — cherry-pick base: 94b96d01e79e 2. `getComputedStyle` + observer/matchMedia stubs (4 tests) — cherry-pick base: 7af577054fd9 3. Router provider + anchor click guard (5 tests) — cherry-pick base: 221c9c353dc8 4. Open-Modal React hang investigation (1 test) 5. CodeEditor: decide wont-fix + keep the failing story or convert to a documented skip (2 tests) ## How to run ``` npx nx run twenty-front-component-renderer:storybook:prebuild cd packages/twenty-front-component-renderer npx vitest run --config vitest.storybook.config.ts --project storybook ```
This commit is contained in:
@@ -105,6 +105,7 @@
|
||||
"{projectRoot}/src/__stories__/html-tag/**/*",
|
||||
"{projectRoot}/src/__stories__/host-api/**/*",
|
||||
"{projectRoot}/src/__stories__/showcase/**/*",
|
||||
"{projectRoot}/src/__stories__/twenty-ui-gallery/**/*",
|
||||
"{projectRoot}/src/__stories__/shared/front-components/**/*",
|
||||
"{workspaceRoot}/packages/twenty-sdk/src/cli/utilities/build/**/*"
|
||||
],
|
||||
|
||||
+6
-1
@@ -16,7 +16,12 @@ const exampleSourcesBuiltPreactDir = path.resolve(
|
||||
'../../src/__stories__/example-sources-built-preact',
|
||||
);
|
||||
|
||||
const SOURCE_SCAN_ROOTS = ['html-tag', 'host-api', 'showcase'];
|
||||
const SOURCE_SCAN_ROOTS = [
|
||||
'html-tag',
|
||||
'host-api',
|
||||
'showcase',
|
||||
'twenty-ui-gallery',
|
||||
];
|
||||
|
||||
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, waitFor, within } from 'storybook/test';
|
||||
|
||||
import {
|
||||
errorHandler,
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/Twenty UI Gallery',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
// Every gallery fixture wraps each component in an error boundary and reports
|
||||
// the aggregated result on the gallery-status element, so a single play
|
||||
// function covers all submodules.
|
||||
const galleryTest: Story['play'] = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const status = await canvas.findByTestId(
|
||||
'gallery-status',
|
||||
{},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(status).toHaveAttribute('data-failed-messages', '');
|
||||
expect(status).toHaveAttribute('data-failed-count', '0');
|
||||
});
|
||||
|
||||
expect(Number(status.getAttribute('data-total-count'))).toBeGreaterThan(0);
|
||||
expect(errorHandler).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const createGalleryStory = (name: string, runtime?: 'preact'): Story => ({
|
||||
args: {
|
||||
componentUrl: getBuiltStoryComponentPathForRender(
|
||||
`${name}.front-component`,
|
||||
runtime,
|
||||
),
|
||||
},
|
||||
play: galleryTest,
|
||||
});
|
||||
|
||||
// Golden known-failure test (TDD): PASSES while the documented sandbox gap
|
||||
// exists — the failing component set matches the expected set EXACTLY. It
|
||||
// FAILS on regression (an unexpected component starts failing), on fix
|
||||
// (nothing fails anymore) and on partial fix (only some expected components
|
||||
// still fail): when your fix lands, flip the story back to the strict
|
||||
// zero-failure `createGalleryStory` play.
|
||||
const createKnownFailureGalleryTest =
|
||||
(expectedFailedComponents: string[]): Story['play'] =>
|
||||
async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const status = await canvas.findByTestId(
|
||||
'gallery-status',
|
||||
{},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
const expectedFailedComponentsSorted = [...expectedFailedComponents].sort();
|
||||
|
||||
// Failure reports arrive asynchronously: retry until the failed set
|
||||
// matches the expected set exactly.
|
||||
await waitFor(() => {
|
||||
const failedComponents = (status.getAttribute('data-failed-names') ?? '')
|
||||
.split(', ')
|
||||
.filter((failedComponent) => failedComponent.length > 0)
|
||||
.sort();
|
||||
|
||||
expect(failedComponents).toEqual(expectedFailedComponentsSorted);
|
||||
});
|
||||
|
||||
expect(errorHandler).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const createKnownFailureGalleryStory = (
|
||||
name: string,
|
||||
expectedFailedComponents: string[],
|
||||
runtime?: 'preact',
|
||||
): Story => ({
|
||||
...createGalleryStory(name, runtime),
|
||||
play: createKnownFailureGalleryTest(expectedFailedComponents),
|
||||
});
|
||||
|
||||
// KNOWN ISSUE (TDD): LinkChip crashes without a router context in the sandbox.
|
||||
export const DataDisplayReact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-data-display-gallery',
|
||||
['LinkChip'],
|
||||
);
|
||||
export const DataDisplayPreact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-data-display-gallery',
|
||||
['LinkChip'],
|
||||
'preact',
|
||||
);
|
||||
|
||||
export const FeedbackReact: Story = createGalleryStory(
|
||||
'twenty-ui-feedback-gallery',
|
||||
);
|
||||
export const FeedbackPreact: Story = createGalleryStory(
|
||||
'twenty-ui-feedback-gallery',
|
||||
'preact',
|
||||
);
|
||||
|
||||
export const IconReact: Story = createGalleryStory('twenty-ui-icon-gallery');
|
||||
export const IconPreact: Story = createGalleryStory(
|
||||
'twenty-ui-icon-gallery',
|
||||
'preact',
|
||||
);
|
||||
|
||||
// KNOWN ISSUE (TDD): base-ui radio internals call MutationObserver.observe,
|
||||
// shipped as an empty stub class by @remote-dom/polyfill.
|
||||
const INPUT_EXPECTED_FAILURES = ['Radio', 'RadioGroup', 'CardPicker'];
|
||||
export const InputReact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-input-gallery',
|
||||
INPUT_EXPECTED_FAILURES,
|
||||
);
|
||||
export const InputPreact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-input-gallery',
|
||||
INPUT_EXPECTED_FAILURES,
|
||||
'preact',
|
||||
);
|
||||
|
||||
// KNOWN ISSUE (TDD): base-ui Collapsible calls getComputedStyle, missing from
|
||||
// the remote-dom Window polyfill. The runtimes diverge (observed
|
||||
// deterministically): under Preact only the first Collapsible consumer
|
||||
// crashes and later siblings render.
|
||||
export const JsonVisualizerReact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-json-visualizer-gallery',
|
||||
['JsonTree', 'JsonArrayNode', 'JsonObjectNode', 'JsonNestedNode'],
|
||||
);
|
||||
export const JsonVisualizerPreact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-json-visualizer-gallery',
|
||||
['JsonTree'],
|
||||
'preact',
|
||||
);
|
||||
|
||||
// KNOWN ISSUE (TDD): same getComputedStyle gap through base-ui Collapsible,
|
||||
// with the same React/Preact divergence.
|
||||
export const LayoutReact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-layout-gallery',
|
||||
['AnimatedEaseInOut', 'AnimatedExpandableContainer'],
|
||||
);
|
||||
export const LayoutPreact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-layout-gallery',
|
||||
['AnimatedEaseInOut'],
|
||||
'preact',
|
||||
);
|
||||
|
||||
// KNOWN ISSUE (TDD): react-router Links crash without a router context.
|
||||
const NAVIGATION_EXPECTED_FAILURES = ['RawLink', 'UndecoratedLink'];
|
||||
export const NavigationReact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-navigation-gallery',
|
||||
NAVIGATION_EXPECTED_FAILURES,
|
||||
);
|
||||
export const NavigationPreact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-navigation-gallery',
|
||||
NAVIGATION_EXPECTED_FAILURES,
|
||||
'preact',
|
||||
);
|
||||
|
||||
// KNOWN ISSUE (TDD): AppTooltip observes document.body with the stubbed-out
|
||||
// MutationObserver.
|
||||
export const SurfacesReact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-surfaces-gallery',
|
||||
['AppTooltip'],
|
||||
);
|
||||
export const SurfacesPreact: Story = createKnownFailureGalleryStory(
|
||||
'twenty-ui-surfaces-gallery',
|
||||
['AppTooltip'],
|
||||
'preact',
|
||||
);
|
||||
|
||||
// KNOWN ISSUE (TDD) golden test: an open Modal (base-ui Dialog portal) hangs
|
||||
// the React-runtime render — the gallery status must never mount. Works under
|
||||
// Preact (see ModalOpenPreact). When fixed, flip this story to the strict
|
||||
// zero-failure play used by ModalOpenPreact.
|
||||
const modalOpenHangTest: Story['play'] = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(
|
||||
canvas.findByTestId('gallery-status', {}, { timeout: 10000 }),
|
||||
).rejects.toThrow();
|
||||
};
|
||||
|
||||
const modalOpenTest: Story['play'] = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const status = await canvas.findByTestId(
|
||||
'gallery-status',
|
||||
{},
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(status).toHaveAttribute('data-failed-messages', '');
|
||||
expect(status).toHaveAttribute('data-failed-count', '0');
|
||||
});
|
||||
|
||||
expect(errorHandler).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
export const ModalOpenReact: Story = {
|
||||
...createGalleryStory('twenty-ui-modal-open-gallery'),
|
||||
play: modalOpenHangTest,
|
||||
};
|
||||
export const ModalOpenPreact: Story = {
|
||||
...createGalleryStory('twenty-ui-modal-open-gallery', 'preact'),
|
||||
play: modalOpenTest,
|
||||
};
|
||||
|
||||
// KNOWN ISSUE (TDD) golden test: monaco cannot load inside the sandbox worker
|
||||
// (no script loading in the polyfilled DOM, opaque-origin CSP): the CodeEditor
|
||||
// wrapper mounts but monaco's onMount never fires. If front components ever
|
||||
// get a supported code editor path, flip the assertion to 'mounted'.
|
||||
const codeEditorTest: Story['play'] = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const codeEditor = await canvas.findByTestId(
|
||||
'code-editor-component',
|
||||
{},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
expect(codeEditor).toHaveAttribute('data-monaco-mount-state', 'pending');
|
||||
};
|
||||
|
||||
export const CodeEditorReact: Story = {
|
||||
...createGalleryStory('twenty-ui-code-editor-gallery'),
|
||||
play: codeEditorTest,
|
||||
};
|
||||
export const CodeEditorPreact: Story = {
|
||||
...createGalleryStory('twenty-ui-code-editor-gallery', 'preact'),
|
||||
play: codeEditorTest,
|
||||
};
|
||||
|
||||
export const TypographyReact: Story = createGalleryStory(
|
||||
'twenty-ui-typography-gallery',
|
||||
);
|
||||
export const TypographyPreact: Story = createGalleryStory(
|
||||
'twenty-ui-typography-gallery',
|
||||
'preact',
|
||||
);
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, waitFor } from 'storybook/test';
|
||||
|
||||
import {
|
||||
errorHandler,
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/Worker Platform APIs',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
// KNOWN ISSUE (TDD) golden test: @remote-dom/polyfill ships MutationObserver
|
||||
// as an empty class, so observe() throws and crashes the fixture at mount.
|
||||
// When a real worker-local MutationObserver lands, flip this play to the
|
||||
// behavior assertions below: click mutation-observer-add, then expect the
|
||||
// data-observed count on mutation-observer-count to become greater than 0 and
|
||||
// errorHandler not to have been called — a no-op stub cannot pass that.
|
||||
const mutationObserverTest: Story['play'] = async () => {
|
||||
// Matching the message keeps an unrelated worker failure (bundle load,
|
||||
// import error, ...) from being cataloged as the MutationObserver gap.
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(errorHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('observe is not a function'),
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
};
|
||||
|
||||
const createStory = (name: string, runtime?: 'preact'): Story => ({
|
||||
args: {
|
||||
componentUrl: getBuiltStoryComponentPathForRender(
|
||||
`${name}.front-component`,
|
||||
runtime,
|
||||
),
|
||||
},
|
||||
play: mutationObserverTest,
|
||||
});
|
||||
|
||||
export const MutationObserverReact: Story = createStory(
|
||||
'mutation-observer-example',
|
||||
);
|
||||
export const MutationObserverPreact: Story = createStory(
|
||||
'mutation-observer-example',
|
||||
'preact',
|
||||
);
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
|
||||
// KNOWN ISSUE (TDD): a react-router Link click must route through the host
|
||||
// navigate API. Today the link cannot even render (no router context in the
|
||||
// sandbox); once it renders, the click must reach hostApi.navigate — and the
|
||||
// host must guard native anchor clicks, which otherwise navigate the host
|
||||
// page before the async worker round-trip can preventDefault.
|
||||
const HostApiRouterLinkFrontComponent = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<FrontComponentCard title="host-api:router-link">
|
||||
<span data-testid="router-link">
|
||||
<UndecoratedLink to="/objects/companies">
|
||||
Go to companies
|
||||
</UndecoratedLink>
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000113',
|
||||
name: 'host-api-router-link',
|
||||
description:
|
||||
'A front component whose react-router link navigates through the host API',
|
||||
component: HostApiRouterLinkFrontComponent,
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, waitFor } from 'storybook/test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
errorHandler,
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { HOST_API_TIMEOUT } from '@/__stories__/shared/test-utils/timeouts';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HostApi/RouterLink',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
// KNOWN ISSUE (TDD) golden test: the fixture's react-router Link crashes at
|
||||
// render (no router context in the sandbox), so the component errors and the
|
||||
// host navigate API is never reached. When a router fix lands, flip this play
|
||||
// to the acceptance assertions: find the link by role, click it, and expect
|
||||
// hostApi.navigate to receive '/objects/companies' — a MemoryRouter cannot
|
||||
// pass that (it renders the link but swallows the click into in-memory
|
||||
// history), and clicking also requires the host to guard native anchor clicks,
|
||||
// which otherwise navigate the host page away before the async worker
|
||||
// round-trip can preventDefault.
|
||||
export const RouterLink: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'host-api-router-link',
|
||||
play: async ({ args }) => {
|
||||
const api = args.frontComponentHostCommunicationApi;
|
||||
|
||||
if (!isDefined(api)) {
|
||||
throw new Error('frontComponentHostCommunicationApi is required');
|
||||
}
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(errorHandler).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(api.navigate).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
export type GalleryEntry = {
|
||||
name: string;
|
||||
node: ReactNode;
|
||||
};
|
||||
|
||||
const GALLERY_STYLE = {
|
||||
padding: 20,
|
||||
backgroundColor: '#ffffff',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 8,
|
||||
maxWidth: 640,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#111827',
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: '#6b7280',
|
||||
};
|
||||
|
||||
const ITEM_STYLE = {
|
||||
display: 'flex',
|
||||
alignItems: 'center' as const,
|
||||
gap: 12,
|
||||
padding: '4px 0',
|
||||
borderBottom: '1px solid #f3f4f6',
|
||||
};
|
||||
|
||||
const ITEM_LABEL_STYLE = {
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: '#6b7280',
|
||||
minWidth: 200,
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const ITEM_FAILED_STYLE = {
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: '#dc2626',
|
||||
};
|
||||
|
||||
type ComponentGalleryProps = {
|
||||
title: string;
|
||||
entries: GalleryEntry[];
|
||||
};
|
||||
|
||||
export const ComponentGallery = ({ title, entries }: ComponentGalleryProps) => {
|
||||
const [failedEntries, setFailedEntries] = useState<
|
||||
{ name: string; message: string }[]
|
||||
>([]);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const reportFailedEntry = (entryName: string, thrownValue: unknown) => {
|
||||
// Error boundaries receive whatever was thrown, not necessarily an Error.
|
||||
const message =
|
||||
thrownValue instanceof Error ? thrownValue.message : String(thrownValue);
|
||||
|
||||
setFailedEntries((previous) =>
|
||||
previous.some((failedEntry) => failedEntry.name === entryName)
|
||||
? previous
|
||||
: [...previous, { name: entryName, message }],
|
||||
);
|
||||
};
|
||||
|
||||
const failedEntryNames = failedEntries.map((failedEntry) => failedEntry.name);
|
||||
|
||||
return (
|
||||
<div data-testid="gallery-root" style={GALLERY_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>{title}</h2>
|
||||
{isMounted && (
|
||||
<span
|
||||
data-testid="gallery-status"
|
||||
data-total-count={entries.length}
|
||||
data-failed-count={failedEntryNames.length}
|
||||
data-failed-names={failedEntryNames.join(', ')}
|
||||
data-failed-messages={failedEntries
|
||||
.map((failedEntry) => `${failedEntry.name}: ${failedEntry.message}`)
|
||||
.join(' | ')}
|
||||
style={STATUS_STYLE}
|
||||
>
|
||||
{failedEntryNames.length === 0
|
||||
? `All ${entries.length} components rendered`
|
||||
: `Failed to render: ${failedEntryNames.join(', ')}`}
|
||||
</span>
|
||||
)}
|
||||
{entries.map((entry) => (
|
||||
<div
|
||||
key={entry.name}
|
||||
data-testid={`gallery-item-${entry.name}`}
|
||||
style={ITEM_STYLE}
|
||||
>
|
||||
<span style={ITEM_LABEL_STYLE}>{entry.name}</span>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<span
|
||||
data-testid={`gallery-item-failed-${entry.name}`}
|
||||
style={ITEM_FAILED_STYLE}
|
||||
>
|
||||
render failed
|
||||
</span>
|
||||
}
|
||||
onError={(error) => reportFailedEntry(entry.name, error)}
|
||||
>
|
||||
{entry.node}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Exercises real MutationObserver semantics inside the sandbox worker: the
|
||||
// observer must fire for React-driven insertions into the observed subtree.
|
||||
const MutationObserverComponent = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [observedMutationCount, setObservedMutationCount] = useState(0);
|
||||
const [items, setItems] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (container === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
const childListRecordCount = records.filter(
|
||||
(record) => record.type === 'childList',
|
||||
).length;
|
||||
|
||||
setObservedMutationCount((previous) => previous + childListRecordCount);
|
||||
});
|
||||
|
||||
observer.observe(container, { childList: true, subtree: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="mutation-observer-component"
|
||||
style={{ fontFamily: 'system-ui, sans-serif', padding: 16 }}
|
||||
>
|
||||
<button
|
||||
data-testid="mutation-observer-add"
|
||||
onClick={() =>
|
||||
setItems((previous) => [...previous, `item-${previous.length}`])
|
||||
}
|
||||
>
|
||||
Add item
|
||||
</button>
|
||||
<div ref={containerRef} data-testid="mutation-observer-container">
|
||||
{items.map((item) => (
|
||||
<span key={item}>{item} </span>
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
data-testid="mutation-observer-count"
|
||||
data-observed={observedMutationCount}
|
||||
>
|
||||
Mutations observed: {observedMutationCount}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000110',
|
||||
name: 'mutation-observer-component',
|
||||
description: 'Asserts MutationObserver works inside the sandbox worker',
|
||||
component: MutationObserverComponent,
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useState } from 'react';
|
||||
import { CodeEditor } from 'twenty-ui/input';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
// KNOWN ISSUE: CodeEditor wraps @monaco-editor/react, which lazy-loads the
|
||||
// monaco runtime through script injection at mount. The sandbox worker has no
|
||||
// script loading (polyfilled DOM, opaque-origin CSP), so monaco can never
|
||||
// become interactive. This fixture documents that empirically: the dedicated
|
||||
// story fails until front components get a supported code editor path.
|
||||
const CodeEditorComponent = () => {
|
||||
const [mountState, setMountState] = useState<'pending' | 'mounted'>(
|
||||
'pending',
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<div
|
||||
data-testid="code-editor-component"
|
||||
data-monaco-mount-state={mountState}
|
||||
style={{ fontFamily: 'system-ui, sans-serif', width: 480 }}
|
||||
>
|
||||
<CodeEditor
|
||||
value={'const greeting = "hello";'}
|
||||
language="typescript"
|
||||
height={200}
|
||||
onMount={() => setMountState('mounted')}
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000112',
|
||||
name: 'twenty-ui-code-editor-gallery',
|
||||
description: 'Renders the monaco-based twenty-ui CodeEditor in the sandbox',
|
||||
component: CodeEditorComponent,
|
||||
});
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
AnimatedCheckmark,
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
AvatarOrIcon,
|
||||
Checkmark,
|
||||
Chip,
|
||||
ColorSample,
|
||||
CommandBlock,
|
||||
LinkChip,
|
||||
NotificationCounter,
|
||||
Pill,
|
||||
Status,
|
||||
StyledTintedIconTileContainer,
|
||||
Tag,
|
||||
TintedIconTile,
|
||||
} from 'twenty-ui/data-display';
|
||||
import { IconStar } from 'twenty-ui/icon';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const DATA_DISPLAY_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'AnimatedCheckmark',
|
||||
node: <AnimatedCheckmark isAnimating size={28} />,
|
||||
},
|
||||
{
|
||||
name: 'Avatar',
|
||||
node: <Avatar placeholder="John Doe" size="md" type="rounded" />,
|
||||
},
|
||||
{
|
||||
name: 'AvatarGroup',
|
||||
node: (
|
||||
<AvatarGroup
|
||||
avatars={[
|
||||
<Avatar key="a" placeholder="Alice" />,
|
||||
<Avatar key="b" placeholder="Bob" />,
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AvatarOrIcon',
|
||||
node: <AvatarOrIcon placeholder="Jane" Icon={IconStar} />,
|
||||
},
|
||||
{
|
||||
name: 'Checkmark',
|
||||
node: <Checkmark />,
|
||||
},
|
||||
{
|
||||
name: 'Chip',
|
||||
node: <Chip label="Chip label" />,
|
||||
},
|
||||
{
|
||||
name: 'ColorSample',
|
||||
node: <ColorSample colorName="blue" />,
|
||||
},
|
||||
{
|
||||
name: 'CommandBlock',
|
||||
node: <CommandBlock commands={['npm install', 'npm run start']} />,
|
||||
},
|
||||
// KNOWN ISSUE (TDD): LinkChip renders a react-router Link and crashes
|
||||
// because the sandbox provides no router context. Expected fix: SDK-injected
|
||||
// Router whose navigator bridges to the host navigate API.
|
||||
{
|
||||
name: 'LinkChip',
|
||||
node: <LinkChip to="/example" label="Link chip" />,
|
||||
},
|
||||
{
|
||||
name: 'NotificationCounter',
|
||||
node: <NotificationCounter count={3} />,
|
||||
},
|
||||
{
|
||||
name: 'Pill',
|
||||
node: <Pill label="Pill" Icon={IconStar} />,
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
node: <Status color="green" text="Active" />,
|
||||
},
|
||||
{
|
||||
name: 'StyledTintedIconTileContainer',
|
||||
node: (
|
||||
<StyledTintedIconTileContainer $dimension="32px">
|
||||
<IconStar size={16} />
|
||||
</StyledTintedIconTileContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Tag',
|
||||
node: <Tag color="blue" text="Tag" />,
|
||||
},
|
||||
{
|
||||
name: 'TintedIconTile',
|
||||
node: <TintedIconTile Icon={IconStar} />,
|
||||
},
|
||||
];
|
||||
|
||||
const DataDisplayGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery
|
||||
title="twenty-ui/data-display"
|
||||
entries={DATA_DISPLAY_ENTRIES}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000101',
|
||||
name: 'twenty-ui-data-display-gallery',
|
||||
description: 'Renders every twenty-ui/data-display component in the sandbox',
|
||||
component: DataDisplayGallery,
|
||||
});
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
AnimatedPlaceholderEmptySubTitle,
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
AnimatedPlaceholderErrorContainer,
|
||||
AnimatedPlaceholderErrorSubTitle,
|
||||
AnimatedPlaceholderErrorTextContainer,
|
||||
AnimatedPlaceholderErrorTitle,
|
||||
Banner,
|
||||
Callout,
|
||||
CircularProgressBar,
|
||||
Info,
|
||||
InlineBanner,
|
||||
Loader,
|
||||
ProgressBar,
|
||||
SidePanelInformationBanner,
|
||||
} from 'twenty-ui/feedback';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const FEEDBACK_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'AnimatedPlaceholder',
|
||||
node: <AnimatedPlaceholder type="error404" />,
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderEmptyContainer',
|
||||
node: (
|
||||
<AnimatedPlaceholderEmptyContainer>
|
||||
Empty
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderEmptyTextContainer',
|
||||
node: (
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
Empty text
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderEmptyTitle',
|
||||
node: (
|
||||
<AnimatedPlaceholderEmptyTitle>No records</AnimatedPlaceholderEmptyTitle>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderEmptySubTitle',
|
||||
node: (
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
Try adding one
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderErrorContainer',
|
||||
node: (
|
||||
<AnimatedPlaceholderErrorContainer>
|
||||
Error
|
||||
</AnimatedPlaceholderErrorContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderErrorTextContainer',
|
||||
node: (
|
||||
<AnimatedPlaceholderErrorTextContainer>
|
||||
Error text
|
||||
</AnimatedPlaceholderErrorTextContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderErrorTitle',
|
||||
node: (
|
||||
<AnimatedPlaceholderErrorTitle>Went wrong</AnimatedPlaceholderErrorTitle>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedPlaceholderErrorSubTitle',
|
||||
node: (
|
||||
<AnimatedPlaceholderErrorSubTitle>
|
||||
Please retry
|
||||
</AnimatedPlaceholderErrorSubTitle>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Banner',
|
||||
node: (
|
||||
<Banner color="blue" variant="primary">
|
||||
Heads up
|
||||
</Banner>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Callout',
|
||||
node: (
|
||||
<Callout variant="info" title="Info" description="A short description." />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'CircularProgressBar',
|
||||
node: <CircularProgressBar size={50} barWidth={5} />,
|
||||
},
|
||||
{
|
||||
name: 'Info',
|
||||
node: <Info accent="blue" text="Some information" />,
|
||||
},
|
||||
{
|
||||
name: 'InlineBanner',
|
||||
node: <InlineBanner color="blue" message="Inline message" />,
|
||||
},
|
||||
{
|
||||
name: 'Loader',
|
||||
node: <Loader color="blue" />,
|
||||
},
|
||||
{
|
||||
name: 'ProgressBar',
|
||||
node: <ProgressBar value={50} ariaLabel="Progress" />,
|
||||
},
|
||||
{
|
||||
name: 'SidePanelInformationBanner',
|
||||
node: <SidePanelInformationBanner message="Panel info" variant="default" />,
|
||||
},
|
||||
];
|
||||
|
||||
const FeedbackGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery title="twenty-ui/feedback" entries={FEEDBACK_ENTRIES} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000102',
|
||||
name: 'twenty-ui-feedback-gallery',
|
||||
description: 'Renders every twenty-ui/feedback component in the sandbox',
|
||||
component: FeedbackGallery,
|
||||
});
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
IconAddressBook,
|
||||
IconBrandAnthropic,
|
||||
IconBrandGemini,
|
||||
IconBrandGroq,
|
||||
IconBrandMistral,
|
||||
IconBrandXai,
|
||||
IconCalendar,
|
||||
IconChartBarHorizontal,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
IconGmail,
|
||||
IconGoogle,
|
||||
IconGoogleCalendar,
|
||||
IconHeart,
|
||||
IconLink,
|
||||
IconLockCustom,
|
||||
IconMail,
|
||||
IconMicrosoft,
|
||||
IconMicrosoftCalendar,
|
||||
IconMicrosoftOutlook,
|
||||
IconModelClaude,
|
||||
IconPhone,
|
||||
IconPlus,
|
||||
IconProviderOpenai,
|
||||
IconRelationManyToOne,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconSparkle2,
|
||||
IconStar,
|
||||
IconTrash,
|
||||
IconTrashXOff,
|
||||
IconTwentyStar,
|
||||
IconTwentyStarFilled,
|
||||
IconUser,
|
||||
IconX,
|
||||
IllustrationIconArray,
|
||||
IllustrationIconCalendarEvent,
|
||||
IllustrationIconCalendarTime,
|
||||
IllustrationIconCurrency,
|
||||
IllustrationIconFile,
|
||||
IllustrationIconJson,
|
||||
IllustrationIconLink,
|
||||
IllustrationIconMail,
|
||||
IllustrationIconManyToMany,
|
||||
IllustrationIconMap,
|
||||
IllustrationIconNumbers,
|
||||
IllustrationIconOneToMany,
|
||||
IllustrationIconOneToOne,
|
||||
IllustrationIconPhone,
|
||||
IllustrationIconSetting,
|
||||
IllustrationIconStar,
|
||||
IllustrationIconTag,
|
||||
IllustrationIconTags,
|
||||
IllustrationIconText,
|
||||
IllustrationIconToggle,
|
||||
IllustrationIconUid,
|
||||
IllustrationIconUser,
|
||||
IllustrationIconWrapper,
|
||||
ThinkingOrbitLoaderIcon,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/icon';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const CUSTOM_ICONS: Record<string, IconComponent> = {
|
||||
IconAddressBook,
|
||||
IconBrandAnthropic,
|
||||
IconBrandGemini,
|
||||
IconBrandGroq,
|
||||
IconBrandMistral,
|
||||
IconBrandXai,
|
||||
IconChartBarHorizontal,
|
||||
IconGmail,
|
||||
IconGoogle,
|
||||
IconGoogleCalendar,
|
||||
IconLockCustom,
|
||||
IconMicrosoft,
|
||||
IconMicrosoftCalendar,
|
||||
IconMicrosoftOutlook,
|
||||
IconModelClaude,
|
||||
IconProviderOpenai,
|
||||
IconRelationManyToOne,
|
||||
IconSparkle2,
|
||||
IconTrashXOff,
|
||||
IconTwentyStar,
|
||||
IconTwentyStarFilled,
|
||||
};
|
||||
|
||||
const ILLUSTRATION_ICONS: Record<string, IconComponent> = {
|
||||
IllustrationIconArray,
|
||||
IllustrationIconCalendarEvent,
|
||||
IllustrationIconCalendarTime,
|
||||
IllustrationIconCurrency,
|
||||
IllustrationIconFile,
|
||||
IllustrationIconJson,
|
||||
IllustrationIconLink,
|
||||
IllustrationIconMail,
|
||||
IllustrationIconManyToMany,
|
||||
IllustrationIconMap,
|
||||
IllustrationIconNumbers,
|
||||
IllustrationIconOneToMany,
|
||||
IllustrationIconOneToOne,
|
||||
IllustrationIconPhone,
|
||||
IllustrationIconSetting,
|
||||
IllustrationIconStar,
|
||||
IllustrationIconTag,
|
||||
IllustrationIconTags,
|
||||
IllustrationIconText,
|
||||
IllustrationIconToggle,
|
||||
IllustrationIconUid,
|
||||
IllustrationIconUser,
|
||||
};
|
||||
|
||||
// Representative sample of the ~400 re-exported Tabler icons: they all share
|
||||
// the same implementation, so rendering each one would only slow the suite.
|
||||
const TABLER_ICON_SAMPLE: Record<string, IconComponent> = {
|
||||
IconCalendar,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
IconHeart,
|
||||
IconLink,
|
||||
IconMail,
|
||||
IconPhone,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconStar,
|
||||
IconTrash,
|
||||
IconUser,
|
||||
IconX,
|
||||
};
|
||||
|
||||
const iconEntries = (icons: Record<string, IconComponent>): GalleryEntry[] =>
|
||||
Object.entries(icons).map(([name, IconComponentToRender]) => ({
|
||||
name,
|
||||
node: <IconComponentToRender size={20} />,
|
||||
}));
|
||||
|
||||
const ICON_ENTRIES: GalleryEntry[] = [
|
||||
...iconEntries(CUSTOM_ICONS),
|
||||
...iconEntries(ILLUSTRATION_ICONS),
|
||||
...iconEntries(TABLER_ICON_SAMPLE),
|
||||
{
|
||||
name: 'IllustrationIconWrapper',
|
||||
node: <IllustrationIconWrapper>i</IllustrationIconWrapper>,
|
||||
},
|
||||
{
|
||||
name: 'ThinkingOrbitLoaderIcon',
|
||||
node: <ThinkingOrbitLoaderIcon size={16} />,
|
||||
},
|
||||
];
|
||||
|
||||
const IconGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery title="twenty-ui/icon" entries={ICON_ENTRIES} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000109',
|
||||
name: 'twenty-ui-icon-gallery',
|
||||
description:
|
||||
'Renders twenty-ui custom and illustration icons plus a Tabler sample in the sandbox',
|
||||
component: IconGallery,
|
||||
});
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { IconPlus, IconSearch, IconStar, IconTrash } from 'twenty-ui/icon';
|
||||
import {
|
||||
AdvancedSettingsToggle,
|
||||
AnimatedButton,
|
||||
AnimatedLightIconButton,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
CardPicker,
|
||||
Checkbox,
|
||||
ColorPickerButton,
|
||||
ColorSchemeCard,
|
||||
ColorSchemePicker,
|
||||
CoreEditorHeader,
|
||||
FloatingButton,
|
||||
FloatingButtonGroup,
|
||||
FloatingIconButton,
|
||||
FloatingIconButtonGroup,
|
||||
IconButton,
|
||||
IconButtonGroup,
|
||||
IconListViewGrip,
|
||||
InsideButton,
|
||||
LightButton,
|
||||
LightIconButton,
|
||||
LightIconButtonGroup,
|
||||
MainButton,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RoundedIconButton,
|
||||
SearchInput,
|
||||
SegmentedControl,
|
||||
Slider,
|
||||
StyledTabContainer,
|
||||
TabButton,
|
||||
TabContent,
|
||||
Toggle,
|
||||
} from 'twenty-ui/input';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const INPUT_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'AdvancedSettingsToggle',
|
||||
node: (
|
||||
<AdvancedSettingsToggle
|
||||
isAdvancedModeEnabled={false}
|
||||
setIsAdvancedModeEnabled={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedButton',
|
||||
node: (
|
||||
<AnimatedButton
|
||||
title="Animated"
|
||||
animatedSvg={<svg width={16} height={16} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedLightIconButton',
|
||||
node: <AnimatedLightIconButton Icon={IconStar} />,
|
||||
},
|
||||
{
|
||||
name: 'Button',
|
||||
node: <Button title="Button" onClick={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'ButtonGroup',
|
||||
node: (
|
||||
<ButtonGroup>
|
||||
{[<Button key="a" title="A" />, <Button key="b" title="B" />]}
|
||||
</ButtonGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'CardPicker',
|
||||
node: (
|
||||
<CardPicker checked={false} handleChange={() => {}}>
|
||||
Card
|
||||
</CardPicker>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Checkbox',
|
||||
node: <Checkbox checked={false} onChange={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'CoreEditorHeader',
|
||||
node: <CoreEditorHeader title="Editor" />,
|
||||
},
|
||||
{
|
||||
name: 'ColorPickerButton',
|
||||
node: <ColorPickerButton colorName="blue" onClick={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'ColorSchemeCard',
|
||||
node: <ColorSchemeCard variant="Light" />,
|
||||
},
|
||||
{
|
||||
name: 'ColorSchemePicker',
|
||||
node: (
|
||||
<ColorSchemePicker
|
||||
value="Light"
|
||||
onChange={() => {}}
|
||||
lightLabel="Light"
|
||||
darkLabel="Dark"
|
||||
systemLabel="System"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'FloatingButton',
|
||||
node: <FloatingButton title="Floating" />,
|
||||
},
|
||||
{
|
||||
name: 'FloatingButtonGroup',
|
||||
node: (
|
||||
<FloatingButtonGroup>
|
||||
{[
|
||||
<FloatingButton key="a" title="A" />,
|
||||
<FloatingButton key="b" title="B" />,
|
||||
]}
|
||||
</FloatingButtonGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'FloatingIconButton',
|
||||
node: <FloatingIconButton Icon={IconSearch} ariaLabel="Search" />,
|
||||
},
|
||||
{
|
||||
name: 'FloatingIconButtonGroup',
|
||||
node: (
|
||||
<FloatingIconButtonGroup
|
||||
iconButtons={[{ Icon: IconSearch, ariaLabel: 'Search' }]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'IconButton',
|
||||
node: <IconButton Icon={IconPlus} ariaLabel="Add" onClick={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'IconButtonGroup',
|
||||
node: (
|
||||
<IconButtonGroup
|
||||
iconButtons={[{ Icon: IconTrash, ariaLabel: 'Delete' }]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'IconListViewGrip',
|
||||
node: <IconListViewGrip />,
|
||||
},
|
||||
{
|
||||
name: 'InsideButton',
|
||||
node: <InsideButton Icon={IconPlus} ariaLabel="Add" />,
|
||||
},
|
||||
{
|
||||
name: 'LightButton',
|
||||
node: <LightButton title="Light" />,
|
||||
},
|
||||
{
|
||||
name: 'LightIconButton',
|
||||
node: <LightIconButton Icon={IconStar} aria-label="Star" />,
|
||||
},
|
||||
{
|
||||
name: 'LightIconButtonGroup',
|
||||
node: (
|
||||
<LightIconButtonGroup
|
||||
iconButtons={[{ Icon: IconStar, ariaLabel: 'Star', onClick: () => {} }]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MainButton',
|
||||
node: <MainButton title="Main" />,
|
||||
},
|
||||
{
|
||||
name: 'Radio',
|
||||
node: <Radio checked={false} label="Radio" />,
|
||||
},
|
||||
{
|
||||
name: 'RadioGroup',
|
||||
node: (
|
||||
<RadioGroup value="a">
|
||||
<Radio value="a" label="A" />
|
||||
<Radio value="b" label="B" />
|
||||
</RadioGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'RoundedIconButton',
|
||||
node: <RoundedIconButton Icon={IconPlus} aria-label="Add" />,
|
||||
},
|
||||
{
|
||||
name: 'SearchInput',
|
||||
node: <SearchInput value="" onChange={() => {}} placeholder="Search" />,
|
||||
},
|
||||
{
|
||||
name: 'SegmentedControl',
|
||||
node: (
|
||||
<SegmentedControl
|
||||
ariaLabel="Choose"
|
||||
value="left"
|
||||
onChange={() => {}}
|
||||
options={[
|
||||
{ value: 'left', label: 'Left' },
|
||||
{ value: 'right', label: 'Right' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Slider',
|
||||
node: <Slider max={100} value={50} onChange={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'StyledTabContainer',
|
||||
node: (
|
||||
<StyledTabContainer>
|
||||
<TabButton id="t1" title="Tab" />
|
||||
</StyledTabContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'TabButton',
|
||||
node: <TabButton id="tab1" title="Tab" />,
|
||||
},
|
||||
{
|
||||
name: 'TabContent',
|
||||
node: <TabContent id="tc1" title="Content" />,
|
||||
},
|
||||
{
|
||||
name: 'Toggle',
|
||||
node: <Toggle value={false} onChange={() => {}} />,
|
||||
},
|
||||
];
|
||||
|
||||
const InputGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery title="twenty-ui/input" entries={INPUT_ENTRIES} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000107',
|
||||
name: 'twenty-ui-input-gallery',
|
||||
description:
|
||||
'Renders every twenty-ui/input component (except monaco CodeEditor) in the sandbox',
|
||||
component: InputGallery,
|
||||
});
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { IconCube } from 'twenty-ui/icon';
|
||||
import {
|
||||
JsonArrayNode,
|
||||
JsonNestedNode,
|
||||
JsonNode,
|
||||
JsonObjectNode,
|
||||
JsonTree,
|
||||
JsonTreeContextProvider,
|
||||
type JsonTreeContextType,
|
||||
JsonValueNode,
|
||||
} from 'twenty-ui/json-visualizer';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const JSON_TREE_CONTEXT_VALUE: JsonTreeContextType = {
|
||||
shouldExpandNodeInitially: () => true,
|
||||
emptyStringLabel: '[empty string]',
|
||||
emptyArrayLabel: '[empty array]',
|
||||
emptyObjectLabel: '[empty object]',
|
||||
arrowButtonCollapsedLabel: 'Expand',
|
||||
arrowButtonExpandedLabel: 'Collapse',
|
||||
};
|
||||
|
||||
const JSON_VISUALIZER_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'JsonTree',
|
||||
node: (
|
||||
<JsonTree
|
||||
value={{ id: 1, name: 'Twenty', tags: ['a', 'b'], active: true }}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
emptyArrayLabel="[empty array]"
|
||||
emptyObjectLabel="[empty object]"
|
||||
emptyStringLabel="[empty string]"
|
||||
arrowButtonCollapsedLabel="Expand"
|
||||
arrowButtonExpandedLabel="Collapse"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'JsonNode',
|
||||
node: (
|
||||
<JsonTreeContextProvider value={JSON_TREE_CONTEXT_VALUE}>
|
||||
<JsonNode value={{ id: 1, name: 'Twenty' }} depth={0} keyPath="" />
|
||||
</JsonTreeContextProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'JsonArrayNode',
|
||||
node: (
|
||||
<JsonTreeContextProvider value={JSON_TREE_CONTEXT_VALUE}>
|
||||
<JsonArrayNode
|
||||
label="items"
|
||||
value={[1, 'two', true]}
|
||||
depth={0}
|
||||
keyPath="items"
|
||||
highlighting={undefined}
|
||||
/>
|
||||
</JsonTreeContextProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'JsonObjectNode',
|
||||
node: (
|
||||
<JsonTreeContextProvider value={JSON_TREE_CONTEXT_VALUE}>
|
||||
<JsonObjectNode
|
||||
label="object"
|
||||
value={{ id: 1, name: 'Twenty' }}
|
||||
depth={0}
|
||||
keyPath="object"
|
||||
highlighting={undefined}
|
||||
/>
|
||||
</JsonTreeContextProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'JsonNestedNode',
|
||||
node: (
|
||||
<JsonTreeContextProvider value={JSON_TREE_CONTEXT_VALUE}>
|
||||
<JsonNestedNode
|
||||
label="nested"
|
||||
Icon={IconCube}
|
||||
elements={[
|
||||
{ id: 'id', label: 'id', value: 1 },
|
||||
{ id: 'name', label: 'name', value: 'Twenty' },
|
||||
]}
|
||||
renderElementsCount={(count) => `{${count}}`}
|
||||
emptyElementsText="[empty object]"
|
||||
depth={0}
|
||||
keyPath="nested"
|
||||
highlighting={undefined}
|
||||
/>
|
||||
</JsonTreeContextProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'JsonValueNode',
|
||||
node: (
|
||||
<JsonTreeContextProvider value={JSON_TREE_CONTEXT_VALUE}>
|
||||
<JsonValueNode valueAsString="Twenty" highlighting={undefined} />
|
||||
</JsonTreeContextProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'JsonTreeContextProvider',
|
||||
node: (
|
||||
<JsonTreeContextProvider value={JSON_TREE_CONTEXT_VALUE}>
|
||||
<JsonNode value="leaf value" depth={0} keyPath="" />
|
||||
</JsonTreeContextProvider>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const JsonVisualizerGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery
|
||||
title="twenty-ui/json-visualizer"
|
||||
entries={JSON_VISUALIZER_ENTRIES}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000106',
|
||||
name: 'twenty-ui-json-visualizer-gallery',
|
||||
description:
|
||||
'Renders every twenty-ui/json-visualizer component in the sandbox',
|
||||
component: JsonVisualizerGallery,
|
||||
});
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { IconHeart, IconStar } from 'twenty-ui/icon';
|
||||
import {
|
||||
AnimatedCircleLoading,
|
||||
AnimatedContainer,
|
||||
AnimatedEaseIn,
|
||||
AnimatedEaseInOut,
|
||||
AnimatedExpandableContainer,
|
||||
AnimatedIconCrossfade,
|
||||
AnimatedRotate,
|
||||
AutogrowWrapper,
|
||||
HorizontalSeparator,
|
||||
ResizeHandle,
|
||||
Section,
|
||||
SectionAlignment,
|
||||
SectionFontColor,
|
||||
} from 'twenty-ui/layout';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const LAYOUT_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'AnimatedCircleLoading',
|
||||
node: <AnimatedCircleLoading>Loading</AnimatedCircleLoading>,
|
||||
},
|
||||
{
|
||||
name: 'AnimatedContainer',
|
||||
node: <AnimatedContainer>Content</AnimatedContainer>,
|
||||
},
|
||||
{
|
||||
name: 'AnimatedEaseIn',
|
||||
node: <AnimatedEaseIn>Fades in</AnimatedEaseIn>,
|
||||
},
|
||||
{
|
||||
name: 'AnimatedEaseInOut',
|
||||
node: <AnimatedEaseInOut isOpen={true}>Panel</AnimatedEaseInOut>,
|
||||
},
|
||||
{
|
||||
name: 'AnimatedExpandableContainer',
|
||||
node: (
|
||||
<AnimatedExpandableContainer isExpanded={true}>
|
||||
Expandable
|
||||
</AnimatedExpandableContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedIconCrossfade',
|
||||
node: (
|
||||
<AnimatedIconCrossfade
|
||||
isActive={true}
|
||||
ActiveIcon={IconStar}
|
||||
InactiveIcon={IconHeart}
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'AnimatedRotate',
|
||||
node: <AnimatedRotate>Rotate</AnimatedRotate>,
|
||||
},
|
||||
{
|
||||
name: 'AutogrowWrapper',
|
||||
node: <AutogrowWrapper>Grows</AutogrowWrapper>,
|
||||
},
|
||||
{
|
||||
name: 'HorizontalSeparator',
|
||||
node: <HorizontalSeparator text="or" />,
|
||||
},
|
||||
{
|
||||
name: 'ResizeHandle',
|
||||
node: <ResizeHandle />,
|
||||
},
|
||||
{
|
||||
name: 'Section',
|
||||
node: (
|
||||
<Section
|
||||
alignment={SectionAlignment.Left}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
Section
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const LayoutGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery title="twenty-ui/layout" entries={LAYOUT_ENTRIES} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000103',
|
||||
name: 'twenty-ui-layout-gallery',
|
||||
description: 'Renders every twenty-ui/layout component in the sandbox',
|
||||
component: LayoutGallery,
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Modal } from 'twenty-ui/surfaces';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
// KNOWN ISSUE: an open Modal (base-ui Dialog portal) hangs the React-runtime
|
||||
// sandbox render entirely (no error, the tree never commits); it works under
|
||||
// Preact. Isolated in its own fixture so the hang cannot mask the rest of the
|
||||
// surfaces gallery.
|
||||
const MODAL_OPEN_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'Modal',
|
||||
node: (
|
||||
<Modal isOpen={true} ariaLabel="Open gallery modal">
|
||||
Modal body
|
||||
</Modal>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const ModalOpenGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery
|
||||
title="twenty-ui/surfaces Modal (open)"
|
||||
entries={MODAL_OPEN_ENTRIES}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000111',
|
||||
name: 'twenty-ui-modal-open-gallery',
|
||||
description: 'Renders an open twenty-ui Modal in the sandbox',
|
||||
component: ModalOpenGallery,
|
||||
});
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { IconHome, IconUser } from 'twenty-ui/icon';
|
||||
import {
|
||||
ClickToActionLink,
|
||||
ContactLink,
|
||||
GithubVersionLink,
|
||||
LinkType,
|
||||
MenuItem,
|
||||
MenuItemAvatar,
|
||||
MenuItemDraggable,
|
||||
MenuItemHotKeys,
|
||||
MenuItemLeftContent,
|
||||
MenuItemMultiSelect,
|
||||
MenuItemMultiSelectAvatar,
|
||||
MenuItemMultiSelectTag,
|
||||
MenuItemNavigate,
|
||||
MenuItemSelect,
|
||||
MenuItemSelectAvatar,
|
||||
MenuItemSelectColor,
|
||||
MenuItemSelectTag,
|
||||
MenuItemSuggestion,
|
||||
MenuItemToggle,
|
||||
MenuPicker,
|
||||
NavigationBar,
|
||||
NavigationBarItem,
|
||||
RawLink,
|
||||
RoundedLink,
|
||||
SocialLink,
|
||||
StyledHoverableMenuItemBase,
|
||||
StyledMenuItemIconCheck,
|
||||
StyledMenuItemLabel,
|
||||
StyledMenuItemLeftContent,
|
||||
StyledMenuItemSelect,
|
||||
UndecoratedLink,
|
||||
} from 'twenty-ui/navigation';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const NAVIGATION_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'ClickToActionLink',
|
||||
node: <ClickToActionLink href="#">Click me</ClickToActionLink>,
|
||||
},
|
||||
{
|
||||
name: 'ContactLink',
|
||||
node: <ContactLink href="https://twenty.com">Contact</ContactLink>,
|
||||
},
|
||||
{
|
||||
name: 'GithubVersionLink',
|
||||
node: <GithubVersionLink version="v1.0.0" />,
|
||||
},
|
||||
{
|
||||
name: 'MenuItem',
|
||||
node: <MenuItem text="Menu item" LeftIcon={IconUser} />,
|
||||
},
|
||||
{
|
||||
name: 'MenuItemLeftContent',
|
||||
node: <MenuItemLeftContent text="Left content" LeftIcon={IconUser} />,
|
||||
},
|
||||
{
|
||||
name: 'StyledHoverableMenuItemBase',
|
||||
node: (
|
||||
<StyledHoverableMenuItemBase>Hoverable base</StyledHoverableMenuItemBase>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'StyledMenuItemIconCheck',
|
||||
node: <StyledMenuItemIconCheck size={16} />,
|
||||
},
|
||||
{
|
||||
name: 'StyledMenuItemLabel',
|
||||
node: <StyledMenuItemLabel>Label</StyledMenuItemLabel>,
|
||||
},
|
||||
{
|
||||
name: 'StyledMenuItemLeftContent',
|
||||
node: (
|
||||
<StyledMenuItemLeftContent>
|
||||
Left content wrapper
|
||||
</StyledMenuItemLeftContent>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemAvatar',
|
||||
node: <MenuItemAvatar text="Avatar item" />,
|
||||
},
|
||||
{
|
||||
name: 'MenuItemDraggable',
|
||||
node: (
|
||||
<MenuItemDraggable
|
||||
text="Draggable"
|
||||
LeftIcon={IconUser}
|
||||
gripMode="always"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemHotKeys',
|
||||
node: <MenuItemHotKeys hotKeys={['⌘', 'K']} />,
|
||||
},
|
||||
{
|
||||
name: 'MenuItemMultiSelect',
|
||||
node: (
|
||||
<MenuItemMultiSelect
|
||||
text="Multi select"
|
||||
selected={false}
|
||||
className=""
|
||||
onSelectChange={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemMultiSelectAvatar',
|
||||
node: (
|
||||
<MenuItemMultiSelectAvatar
|
||||
text="Multi avatar"
|
||||
selected={true}
|
||||
onSelectChange={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemMultiSelectTag',
|
||||
node: (
|
||||
<MenuItemMultiSelectTag
|
||||
text="Tag"
|
||||
color="blue"
|
||||
selected={false}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemNavigate',
|
||||
node: (
|
||||
<MenuItemNavigate
|
||||
text="Navigate"
|
||||
LeftIcon={IconUser}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'StyledMenuItemSelect',
|
||||
node: <StyledMenuItemSelect>Select base</StyledMenuItemSelect>,
|
||||
},
|
||||
{
|
||||
name: 'MenuItemSelect',
|
||||
node: <MenuItemSelect text="Select" selected={true} onClick={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'MenuItemSelectAvatar',
|
||||
node: (
|
||||
<MenuItemSelectAvatar
|
||||
text="Select avatar"
|
||||
selected={true}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemSelectColor',
|
||||
node: (
|
||||
<MenuItemSelectColor color="blue" selected={true} onClick={() => {}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemSelectTag',
|
||||
node: (
|
||||
<MenuItemSelectTag
|
||||
color="blue"
|
||||
text="Select tag"
|
||||
selected={true}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuItemSuggestion',
|
||||
node: <MenuItemSuggestion text="Suggestion" onClick={() => {}} />,
|
||||
},
|
||||
{
|
||||
name: 'MenuItemToggle',
|
||||
node: (
|
||||
<MenuItemToggle text="Toggle" toggled={true} onToggleChange={() => {}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MenuPicker',
|
||||
node: <MenuPicker id="picker-1" icon={IconHome} label="Picker" />,
|
||||
},
|
||||
{
|
||||
name: 'NavigationBar',
|
||||
node: (
|
||||
<NavigationBar
|
||||
activeItemName="home"
|
||||
items={[
|
||||
{ name: 'home', label: 'Home', Icon: IconHome, onClick: () => {} },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'NavigationBarItem',
|
||||
node: (
|
||||
<NavigationBarItem
|
||||
Icon={IconHome}
|
||||
isActive={true}
|
||||
ariaLabel="Home"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// KNOWN ISSUE (TDD): RawLink and UndecoratedLink render a react-router Link
|
||||
// and crash because the sandbox provides no router context. Expected fix:
|
||||
// SDK-injected Router whose navigator bridges to the host navigate API.
|
||||
{
|
||||
name: 'RawLink',
|
||||
node: <RawLink href="/path">Raw link</RawLink>,
|
||||
},
|
||||
{
|
||||
name: 'RoundedLink',
|
||||
node: <RoundedLink href="https://twenty.com" label="Rounded link" />,
|
||||
},
|
||||
{
|
||||
name: 'SocialLink',
|
||||
node: (
|
||||
<SocialLink href="https://twitter.com/twenty" type={LinkType.Twitter} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'UndecoratedLink',
|
||||
node: <UndecoratedLink to="/path">Undecorated link</UndecoratedLink>,
|
||||
},
|
||||
];
|
||||
|
||||
const NavigationGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery
|
||||
title="twenty-ui/navigation"
|
||||
entries={NAVIGATION_ENTRIES}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000108',
|
||||
name: 'twenty-ui-navigation-gallery',
|
||||
description: 'Renders every twenty-ui/navigation component in the sandbox',
|
||||
component: NavigationGallery,
|
||||
});
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
AppTooltip,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Modal,
|
||||
ModalBackdrop,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/surfaces';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const SURFACES_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'AppTooltip',
|
||||
node: (
|
||||
<>
|
||||
<span id="gallery-tooltip-anchor">Tooltip anchor</span>
|
||||
<AppTooltip
|
||||
anchorSelect="#gallery-tooltip-anchor"
|
||||
content="Tooltip content"
|
||||
isOpen={true}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Card',
|
||||
node: <Card>Card body</Card>,
|
||||
},
|
||||
{
|
||||
name: 'CardContent',
|
||||
node: <CardContent>Card content</CardContent>,
|
||||
},
|
||||
{
|
||||
name: 'CardFooter',
|
||||
node: <CardFooter>Card footer</CardFooter>,
|
||||
},
|
||||
{
|
||||
name: 'CardHeader',
|
||||
node: <CardHeader>Card header</CardHeader>,
|
||||
},
|
||||
// Rendered closed here so a hang cannot mask the rest of this gallery; the
|
||||
// open-Modal known issue is exposed by twenty-ui-modal-open-gallery.
|
||||
{
|
||||
name: 'Modal',
|
||||
node: (
|
||||
<Modal isOpen={false} ariaLabel="Gallery modal">
|
||||
Modal body
|
||||
</Modal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'ModalBackdrop',
|
||||
node: (
|
||||
<ModalBackdrop overlay="dark" backdropZIndex={39}>
|
||||
Backdrop child
|
||||
</ModalBackdrop>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'ModalContent',
|
||||
node: <ModalContent>Modal content</ModalContent>,
|
||||
},
|
||||
{
|
||||
name: 'ModalFooter',
|
||||
node: <ModalFooter>Modal footer</ModalFooter>,
|
||||
},
|
||||
{
|
||||
name: 'ModalHeader',
|
||||
node: <ModalHeader>Modal header</ModalHeader>,
|
||||
},
|
||||
{
|
||||
name: 'OverflowingTextWithTooltip',
|
||||
node: <OverflowingTextWithTooltip text="Some overflowing text" />,
|
||||
},
|
||||
];
|
||||
|
||||
const SurfacesGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery title="twenty-ui/surfaces" entries={SURFACES_ENTRIES} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000105',
|
||||
name: 'twenty-ui-surfaces-gallery',
|
||||
description: 'Renders every twenty-ui/surfaces component in the sandbox',
|
||||
component: SurfacesGallery,
|
||||
});
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
VisibilityHidden,
|
||||
VisibilityHiddenInput,
|
||||
} from 'twenty-ui/accessibility';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
H2Title,
|
||||
H3Title,
|
||||
Label,
|
||||
LinkifiedText,
|
||||
SeparatorLineText,
|
||||
StyledText,
|
||||
StyledTextContent,
|
||||
StyledTextWrapper,
|
||||
} from 'twenty-ui/typography';
|
||||
|
||||
import {
|
||||
ComponentGallery,
|
||||
type GalleryEntry,
|
||||
} from '../shared/front-components/component-gallery';
|
||||
|
||||
const TYPOGRAPHY_ENTRIES: GalleryEntry[] = [
|
||||
{
|
||||
name: 'H1Title',
|
||||
node: <H1Title title="Heading 1" fontColor={H1TitleFontColor.Primary} />,
|
||||
},
|
||||
{
|
||||
name: 'H2Title',
|
||||
node: <H2Title title="Heading 2" />,
|
||||
},
|
||||
{
|
||||
name: 'H3Title',
|
||||
node: <H3Title title="Heading 3" />,
|
||||
},
|
||||
{
|
||||
name: 'Label',
|
||||
node: <Label variant="default">Label</Label>,
|
||||
},
|
||||
{
|
||||
name: 'LinkifiedText',
|
||||
node: <LinkifiedText text="Visit https://twenty.com now" />,
|
||||
},
|
||||
{
|
||||
name: 'SeparatorLineText',
|
||||
node: <SeparatorLineText>or</SeparatorLineText>,
|
||||
},
|
||||
{
|
||||
name: 'StyledText',
|
||||
node: <StyledText text="Styled text" />,
|
||||
},
|
||||
{
|
||||
name: 'StyledTextContent',
|
||||
node: <StyledTextContent>Content</StyledTextContent>,
|
||||
},
|
||||
{
|
||||
name: 'StyledTextWrapper',
|
||||
node: <StyledTextWrapper>Wrapper</StyledTextWrapper>,
|
||||
},
|
||||
{
|
||||
name: 'VisibilityHidden',
|
||||
node: <VisibilityHidden>Screen-reader only</VisibilityHidden>,
|
||||
},
|
||||
{
|
||||
name: 'VisibilityHiddenInput',
|
||||
node: <VisibilityHiddenInput readOnly value="" />,
|
||||
},
|
||||
];
|
||||
|
||||
const TypographyGallery = () => (
|
||||
<ThemeProvider colorScheme="light">
|
||||
<ComponentGallery
|
||||
title="twenty-ui/typography + accessibility"
|
||||
entries={TYPOGRAPHY_ENTRIES}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000104',
|
||||
name: 'twenty-ui-typography-gallery',
|
||||
description:
|
||||
'Renders every twenty-ui/typography and accessibility component in the sandbox',
|
||||
component: TypographyGallery,
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
// Re-exported so @storybook/addon-vitest's in-UI "Run tests" button (which
|
||||
// only discovers vitest.config.* / vite.config.* files) finds the storybookTest
|
||||
// project. CLI runs keep passing --config vitest.storybook.config.ts.
|
||||
export { default } from './vitest.storybook.config';
|
||||
Reference in New Issue
Block a user