Migrate twenty UI (#21407)
## Migrate all `twenty-ui-deprecated` components into `twenty-ui` Ports all **192 components** and **70 stories** into the new `twenty-ui` package with full public-API parity (export diff: 0 missing / 0 extra across all 13 modules; story titles byte-identical for the Argos cross-package diff). - **Styling:** Linaria → SCSS Modules, `var(--t-*)` tokens, `data-*` state. Canonical pattern in `Button.module.scss`. - **Behavior:** Base UI where mapped (Checkbox, Radio, Modal→Dialog, Tooltip drops `react-tooltip`, JSON tree→Collapsible); framer kept only where animation is the public contract. - **Fixed an inert axe gate** in `.storybook/vitest.setup.ts` (a11y addon annotations were never registered). Now live; 119 stories carry `a11y: 'todo'` overrides pending a fix pass.
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { JsonTree } from '@ui/json-visualizer/components/JsonTree';
|
||||
import { isTwoFirstDepths } from '@ui/json-visualizer/utils/isTwoFirstDepths';
|
||||
import {
|
||||
expect,
|
||||
fn,
|
||||
userEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from 'storybook/test';
|
||||
|
||||
const meta: Meta<typeof JsonTree> = {
|
||||
title: 'UI/JsonVisualizer/JsonTree',
|
||||
component: JsonTree,
|
||||
args: {
|
||||
shouldExpandNodeInitially: () => true,
|
||||
emptyArrayLabel: 'Empty Array',
|
||||
emptyObjectLabel: 'Empty Object',
|
||||
emptyStringLabel: '[empty string]',
|
||||
arrowButtonCollapsedLabel: 'Expand',
|
||||
arrowButtonExpandedLabel: 'Collapse',
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof JsonTree>;
|
||||
|
||||
export const String: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: 'Hello',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const node = await canvas.findByText('Hello');
|
||||
|
||||
expect(node).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const StringWithSpecialCharacters: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: 'Merry \n Christmas \t 🎄',
|
||||
onNodeValueClick: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const node = await canvas.findByText('Merry Christmas 🎄');
|
||||
|
||||
await userEvent.click(node);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onNodeValueClick).toHaveBeenCalledWith(
|
||||
'Merry \n Christmas \t 🎄',
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const Number: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: 42,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const node = await canvas.findByText('42');
|
||||
|
||||
expect(node).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const Boolean: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const node = await canvas.findByText('true');
|
||||
|
||||
expect(node).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const Null: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: null,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const node = await canvas.findByText('null');
|
||||
|
||||
expect(node).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArraySimple: Story = {
|
||||
args: {
|
||||
value: [1, 2, 3],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const node = await canvas.findByText('3');
|
||||
|
||||
expect(node).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayEmpty: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: [],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Array');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayNested: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: [1, 2, ['a', 'b', 'c'], 3],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedArrayElements = await canvas.findByText('[3]');
|
||||
|
||||
expect(nestedArrayElements).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayNestedEmpty: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: [1, 2, [], 3],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedArrayElements = await canvas.findByText('[0]');
|
||||
|
||||
expect(nestedArrayElements).toBeVisible();
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Array');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayWithObjects: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: [
|
||||
{
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
{
|
||||
name: 'John Dowl',
|
||||
age: 42,
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
|
||||
|
||||
expect(nestedObjectItemsCounts).toHaveLength(2);
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectSimple: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const name = await canvas.findByText('John Doe');
|
||||
expect(name).toBeVisible();
|
||||
|
||||
const age = await canvas.findByText('30');
|
||||
expect(age).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectEmpty: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Object');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectNested: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
},
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
|
||||
|
||||
expect(nestedObjectItemsCounts).toHaveLength(2);
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectNestedEmpty: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
person: {},
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCount = await canvas.findByText('{0}');
|
||||
|
||||
expect(nestedObjectItemsCount).toBeVisible();
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Object');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectWithArray: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
users: [
|
||||
{ id: 1, name: 'John' },
|
||||
{ id: 2, name: 'Jane' },
|
||||
],
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedArrayCount = await canvas.findByText('[2]');
|
||||
expect(nestedArrayCount).toBeVisible();
|
||||
|
||||
const nestedObjectCounts = await canvas.findAllByText('{2}');
|
||||
expect(nestedObjectCounts).toHaveLength(3);
|
||||
},
|
||||
};
|
||||
|
||||
export const NestedElementCanBeCollapsed: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
age: 12,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const toggleButton = await canvas.findByRole('button', {
|
||||
name: 'Collapse',
|
||||
});
|
||||
|
||||
const ageElement = canvas.getByText('age');
|
||||
|
||||
await Promise.all([
|
||||
waitForElementToBeRemoved(ageElement),
|
||||
|
||||
userEvent.click(toggleButton),
|
||||
]);
|
||||
|
||||
expect(toggleButton).toHaveTextContent('Expand');
|
||||
},
|
||||
};
|
||||
|
||||
export const ExpandingElementExpandsAllItsDescendants: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
country: {
|
||||
name: 'USA',
|
||||
code: 'US',
|
||||
},
|
||||
},
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
{
|
||||
const allCollapseButtons = await canvas.findAllByRole('button', {
|
||||
name: 'Collapse',
|
||||
});
|
||||
|
||||
expect(allCollapseButtons).toHaveLength(3);
|
||||
|
||||
for (const collapseButton of allCollapseButtons.reverse()) {
|
||||
await userEvent.click(collapseButton);
|
||||
}
|
||||
}
|
||||
|
||||
const rootExpandButton = await canvas.findByRole('button', {
|
||||
name: 'Expand',
|
||||
});
|
||||
|
||||
await userEvent.click(rootExpandButton);
|
||||
|
||||
{
|
||||
const allCollapseButtons = await canvas.findAllByRole('button', {
|
||||
name: 'Collapse',
|
||||
});
|
||||
|
||||
expect(allCollapseButtons).toHaveLength(3);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ExpandTwoFirstDepths: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
country: {
|
||||
name: 'USA',
|
||||
code: 'US',
|
||||
},
|
||||
},
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
shouldExpandNodeInitially: isTwoFirstDepths,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nameElement = await canvas.findByText('name');
|
||||
expect(nameElement).toBeVisible();
|
||||
|
||||
const addressElement = await canvas.findByText('address');
|
||||
expect(addressElement).toBeVisible();
|
||||
|
||||
const streetElement = canvas.queryByText('street');
|
||||
expect(streetElement).not.toBeInTheDocument();
|
||||
|
||||
const countrCodeElement = canvas.queryByText('code');
|
||||
expect(countrCodeElement).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const ReallyDeepNestedObject: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: {
|
||||
g: {
|
||||
h: {
|
||||
i: {
|
||||
j: {
|
||||
k: {
|
||||
l: {
|
||||
m: {
|
||||
n: {
|
||||
o: {
|
||||
p: {
|
||||
q: {
|
||||
r: {
|
||||
s: {
|
||||
t: {
|
||||
u: {
|
||||
v: {
|
||||
w: {
|
||||
x: {
|
||||
y: {
|
||||
z: {
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bis: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: {
|
||||
g: {
|
||||
h: {
|
||||
i: {
|
||||
j: {
|
||||
k: {
|
||||
l: {
|
||||
m: {
|
||||
n: {
|
||||
o: {
|
||||
p: {
|
||||
q: {
|
||||
r: {
|
||||
s: {
|
||||
t: {
|
||||
u: {
|
||||
v: {
|
||||
w: {
|
||||
x: {
|
||||
y: {
|
||||
z: {
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const finalNodes = await canvas.findAllByText('end');
|
||||
|
||||
expect(finalNodes).toHaveLength(2);
|
||||
expect(finalNodes[0]).toBeVisible();
|
||||
expect(finalNodes[1]).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const LongText: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.':
|
||||
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = await canvas.findByText(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.',
|
||||
);
|
||||
|
||||
expect(label).toBeVisible();
|
||||
|
||||
const value = await canvas.findByText(
|
||||
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
|
||||
);
|
||||
|
||||
expect(value).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const BlueHighlighting: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
getNodeHighlighting: () => 'blue',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const ageElement = await canvas.findByText('age');
|
||||
expect(ageElement).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const PartialBlueHighlighting: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
address: {
|
||||
city: 'Paris',
|
||||
},
|
||||
},
|
||||
getNodeHighlighting: (keyPath: string) =>
|
||||
keyPath === 'address' ? 'partial-blue' : undefined,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const ageElement = await canvas.findByText('age');
|
||||
expect(ageElement).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const RedHighlighting: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
address: {
|
||||
city: 'Paris',
|
||||
},
|
||||
},
|
||||
getNodeHighlighting: () => 'red',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const ageElement = await canvas.findByText('age');
|
||||
expect(ageElement).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const CopyJsonNodeValue: Story = {
|
||||
// TODO(a11y): violations inherited from deprecated story; fix during a11y pass
|
||||
parameters: { a11y: { test: 'todo' } },
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
onNodeValueClick: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nameValue = await canvas.findByText('John Doe');
|
||||
|
||||
await userEvent.click(nameValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onNodeValueClick).toHaveBeenCalledWith('John Doe');
|
||||
});
|
||||
|
||||
const ageValue = await canvas.findByText('30');
|
||||
|
||||
await userEvent.click(ageValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onNodeValueClick).toHaveBeenCalledWith('30');
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { IconBrackets } from '@ui/display';
|
||||
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { type JsonArray } from 'type-fest';
|
||||
|
||||
export const JsonArrayNode = ({
|
||||
label,
|
||||
value,
|
||||
depth,
|
||||
keyPath,
|
||||
highlighting,
|
||||
}: {
|
||||
label?: string;
|
||||
value: JsonArray;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { emptyArrayLabel } = useJsonTreeContextOrThrow();
|
||||
|
||||
return (
|
||||
<JsonNestedNode
|
||||
elements={[...value.entries()].map(([key, value]) => ({
|
||||
id: key,
|
||||
label: String(key),
|
||||
value,
|
||||
}))}
|
||||
renderElementsCount={(count) => `[${count}]`}
|
||||
label={label}
|
||||
Icon={IconBrackets}
|
||||
depth={depth}
|
||||
emptyElementsText={emptyArrayLabel}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
.container {
|
||||
display: grid;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.labelContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.elementsCount {
|
||||
color: var(--t-font-color-tertiary);
|
||||
}
|
||||
|
||||
.elementsCountRed {
|
||||
color: var(--t-font-color-danger);
|
||||
}
|
||||
|
||||
// Base UI sets --collapsible-panel-height to a pixel value during
|
||||
// open/close transitions and back to auto once fully open, so nested
|
||||
// nodes can still resize their ancestors.
|
||||
.panel {
|
||||
height: var(--collapsible-panel-height);
|
||||
overflow: clip;
|
||||
transition:
|
||||
height 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
|
||||
&[data-starting-style],
|
||||
&[data-ending-style] {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
row-gap: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.nested {
|
||||
padding-left: var(--t-spacing-8);
|
||||
|
||||
> :first-of-type {
|
||||
margin-top: var(--t-spacing-2);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
declare const classNames: {
|
||||
readonly container: 'container';
|
||||
readonly labelContainer: 'labelContainer';
|
||||
readonly elementsCount: 'elementsCount';
|
||||
readonly elementsCountRed: 'elementsCountRed';
|
||||
readonly panel: 'panel';
|
||||
readonly list: 'list';
|
||||
readonly nested: 'nested';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Collapsible } from '@base-ui/react/collapsible';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { clsx } from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from '@ui/utilities/utils/isDefined';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
import { type IconComponent } from '@ui/display';
|
||||
import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow';
|
||||
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
|
||||
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
|
||||
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
|
||||
import styles from './JsonNestedNode.module.scss';
|
||||
|
||||
export const JsonNestedNode = ({
|
||||
label,
|
||||
Icon,
|
||||
elements,
|
||||
renderElementsCount,
|
||||
emptyElementsText,
|
||||
depth,
|
||||
keyPath,
|
||||
highlighting,
|
||||
}: {
|
||||
label?: string;
|
||||
Icon: IconComponent;
|
||||
elements: Array<{ id: string | number; label: string; value: JsonValue }>;
|
||||
renderElementsCount?: (count: number) => string;
|
||||
emptyElementsText: string;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
highlighting?: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
|
||||
|
||||
const hideRoot = !isDefined(label);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(
|
||||
shouldExpandNodeInitially({ keyPath, depth }),
|
||||
);
|
||||
|
||||
const renderedChildren = (
|
||||
<ul className={clsx(styles.list, depth > 0 && styles.nested)}>
|
||||
{elements.length === 0 ? (
|
||||
<JsonNodeValue valueAsString={emptyElementsText} />
|
||||
) : (
|
||||
elements.map(({ id, label, value }) => {
|
||||
const nextKeyPath = isNonEmptyString(keyPath)
|
||||
? `${keyPath}.${id}`
|
||||
: String(id);
|
||||
|
||||
return (
|
||||
<JsonNode
|
||||
key={id}
|
||||
label={label}
|
||||
value={value}
|
||||
depth={depth + 1}
|
||||
keyPath={nextKeyPath}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const handleArrowClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
if (hideRoot) {
|
||||
return <li className={styles.container}>{renderedChildren}</li>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
className={styles.container}
|
||||
open={isOpen}
|
||||
render={<li />}
|
||||
>
|
||||
<div className={styles.labelContainer}>
|
||||
<JsonArrow
|
||||
isOpen={isOpen}
|
||||
onClick={handleArrowClick}
|
||||
variant={
|
||||
highlighting === 'partial-blue'
|
||||
? 'blue'
|
||||
: highlighting === 'red'
|
||||
? highlighting
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<JsonNodeLabel
|
||||
label={label}
|
||||
Icon={Icon}
|
||||
highlighting={highlighting === 'red' ? highlighting : undefined}
|
||||
/>
|
||||
|
||||
{renderElementsCount && (
|
||||
<span
|
||||
className={clsx(
|
||||
styles.elementsCount,
|
||||
highlighting === 'red' && styles.elementsCountRed,
|
||||
)}
|
||||
>
|
||||
{renderElementsCount(elements.length)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Collapsible.Panel className={styles.panel}>
|
||||
{renderedChildren}
|
||||
</Collapsible.Panel>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
isBoolean,
|
||||
isNonEmptyString,
|
||||
isNumber,
|
||||
isString,
|
||||
} from '@sniptt/guards';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconCircleOff,
|
||||
IconNumber9,
|
||||
IconTypography,
|
||||
} from '@ui/display';
|
||||
import { JsonArrayNode } from '@ui/json-visualizer/components/JsonArrayNode';
|
||||
import { JsonObjectNode } from '@ui/json-visualizer/components/JsonObjectNode';
|
||||
import { JsonValueNode } from '@ui/json-visualizer/components/JsonValueNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { isArray } from '@ui/json-visualizer/utils/isArray';
|
||||
import { isDefined } from '@ui/utilities/utils/isDefined';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
export const JsonNode = ({
|
||||
label,
|
||||
value,
|
||||
depth,
|
||||
keyPath,
|
||||
}: {
|
||||
label?: string;
|
||||
value: JsonValue;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
}) => {
|
||||
const { getNodeHighlighting, emptyStringLabel } = useJsonTreeContextOrThrow();
|
||||
|
||||
const highlighting = getNodeHighlighting?.(keyPath);
|
||||
|
||||
if (!isDefined(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString="null"
|
||||
Icon={IconCircleOff}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isString(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString={isNonEmptyString(value) ? value : emptyStringLabel}
|
||||
Icon={IconTypography}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNumber(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString={String(value)}
|
||||
Icon={IconNumber9}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isBoolean(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString={String(value)}
|
||||
Icon={IconCheckbox}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isArray(value)) {
|
||||
return (
|
||||
<JsonArrayNode
|
||||
label={label}
|
||||
value={value}
|
||||
depth={depth}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonObjectNode
|
||||
label={label}
|
||||
value={value}
|
||||
depth={depth}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { IconCube } from '@ui/display';
|
||||
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { type JsonObject } from 'type-fest';
|
||||
|
||||
export const JsonObjectNode = ({
|
||||
label,
|
||||
value,
|
||||
depth,
|
||||
keyPath,
|
||||
highlighting,
|
||||
}: {
|
||||
label?: string;
|
||||
value: JsonObject;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { emptyObjectLabel } = useJsonTreeContextOrThrow();
|
||||
|
||||
return (
|
||||
<JsonNestedNode
|
||||
elements={Object.entries(value).map(([key, value]) => ({
|
||||
id: key,
|
||||
label: key,
|
||||
value,
|
||||
}))}
|
||||
renderElementsCount={(count) => `{${count}}`}
|
||||
label={label}
|
||||
Icon={IconCube}
|
||||
depth={depth}
|
||||
emptyElementsText={emptyObjectLabel}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
|
||||
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
||||
import { JsonTreeContextProvider } from '@ui/json-visualizer/components/JsonTreeContextProvider';
|
||||
import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
export const JsonTree = ({
|
||||
value,
|
||||
getNodeHighlighting,
|
||||
shouldExpandNodeInitially,
|
||||
emptyArrayLabel,
|
||||
emptyObjectLabel,
|
||||
emptyStringLabel,
|
||||
arrowButtonCollapsedLabel,
|
||||
arrowButtonExpandedLabel,
|
||||
onNodeValueClick,
|
||||
}: {
|
||||
value: JsonValue;
|
||||
getNodeHighlighting?: GetJsonNodeHighlighting;
|
||||
shouldExpandNodeInitially: (
|
||||
params: ShouldExpandNodeInitiallyProps,
|
||||
) => boolean;
|
||||
emptyArrayLabel: string;
|
||||
emptyObjectLabel: string;
|
||||
emptyStringLabel: string;
|
||||
arrowButtonCollapsedLabel: string;
|
||||
arrowButtonExpandedLabel: string;
|
||||
onNodeValueClick?: (valueAsString: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<JsonTreeContextProvider
|
||||
value={{
|
||||
getNodeHighlighting,
|
||||
shouldExpandNodeInitially,
|
||||
emptyArrayLabel,
|
||||
emptyObjectLabel,
|
||||
emptyStringLabel,
|
||||
arrowButtonCollapsedLabel,
|
||||
arrowButtonExpandedLabel,
|
||||
onNodeValueClick,
|
||||
}}
|
||||
>
|
||||
<JsonList depth={0}>
|
||||
<JsonNode value={value} depth={0} keyPath="" />
|
||||
</JsonList>
|
||||
</JsonTreeContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
JsonTreeContext,
|
||||
type JsonTreeContextType,
|
||||
} from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
|
||||
export const JsonTreeContextProvider = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: JsonTreeContextType;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<JsonTreeContext.Provider value={value}>
|
||||
{children}
|
||||
</JsonTreeContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.listItem {
|
||||
align-items: center;
|
||||
column-gap: var(--t-spacing-2);
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
declare const classNames: {
|
||||
readonly listItem: 'listItem';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { type IconComponent } from '@ui/display';
|
||||
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
|
||||
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
|
||||
import styles from './JsonValueNode.module.scss';
|
||||
|
||||
type JsonValueNodeProps = {
|
||||
valueAsString: string;
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
} & (
|
||||
| {
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
}
|
||||
| {
|
||||
label?: never;
|
||||
Icon?: unknown;
|
||||
}
|
||||
);
|
||||
|
||||
export const JsonValueNode = (props: JsonValueNodeProps) => {
|
||||
return (
|
||||
<li className={styles.listItem}>
|
||||
{props.label && (
|
||||
<JsonNodeLabel
|
||||
label={props.label}
|
||||
Icon={props.Icon}
|
||||
highlighting={props.highlighting}
|
||||
/>
|
||||
)}
|
||||
|
||||
<JsonNodeValue
|
||||
valueAsString={props.valueAsString}
|
||||
highlighting={props.highlighting}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
.button {
|
||||
align-items: center;
|
||||
background-color: var(--t-background-transparent-lighter);
|
||||
border-color: var(--t-border-color-medium);
|
||||
border-radius: var(--t-border-radius-sm);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-inline: var(--t-spacing-1);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.red {
|
||||
background-color: var(--t-background-danger);
|
||||
border-color: var(--t-border-color-danger);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&[data-open] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
declare const classNames: {
|
||||
readonly button: 'button';
|
||||
readonly red: 'red';
|
||||
readonly chevron: 'chevron';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { VisibilityHidden } from '@ui/accessibility';
|
||||
import { IconChevronDown } from '@ui/display';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
|
||||
import { clsx } from 'clsx';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import styles from './JsonArrow.module.scss';
|
||||
|
||||
export const JsonArrow = ({
|
||||
isOpen,
|
||||
onClick,
|
||||
variant,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClick: () => void;
|
||||
variant?: 'blue' | 'red';
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { arrowButtonCollapsedLabel, arrowButtonExpandedLabel } =
|
||||
useJsonTreeContextOrThrow();
|
||||
|
||||
const iconColor =
|
||||
variant === 'blue'
|
||||
? themeCssVariables.color.blue
|
||||
: variant === 'red'
|
||||
? themeCssVariables.font.color.danger
|
||||
: themeCssVariables.font.color.secondary;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(styles.button, variant === 'red' && styles.red)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<VisibilityHidden>
|
||||
{isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel}
|
||||
</VisibilityHidden>
|
||||
|
||||
<div className={styles.chevron} data-open={isOpen || undefined}>
|
||||
<IconChevronDown size={theme.icon.size.md} color={iconColor} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: grid;
|
||||
row-gap: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.nested {
|
||||
padding-left: var(--t-spacing-8);
|
||||
|
||||
> :first-of-type {
|
||||
margin-top: var(--t-spacing-2);
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
declare const classNames: {
|
||||
readonly list: 'list';
|
||||
readonly nested: 'nested';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import styles from './JsonList.module.scss';
|
||||
|
||||
export const JsonList = ({
|
||||
depth,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
depth: number;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => (
|
||||
<ul className={clsx(styles.list, depth > 0 && styles.nested, className)}>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
.listItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
declare const classNames: {
|
||||
readonly listItem: 'listItem';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import styles from './JsonListItem.module.scss';
|
||||
|
||||
export const JsonListItem = ({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => <li className={clsx(styles.listItem, className)}>{children}</li>;
|
||||
@@ -0,0 +1,34 @@
|
||||
.labelContainer {
|
||||
background-color: var(--t-background-transparent-lighter);
|
||||
border-color: var(--t-border-color-medium);
|
||||
color: var(--t-font-color-primary);
|
||||
border-radius: var(--t-border-radius-sm);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
column-gap: var(--t-spacing-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
font-size: var(--t-font-size-md);
|
||||
white-space: nowrap;
|
||||
padding-inline: var(--t-spacing-2);
|
||||
|
||||
> span {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: var(--t-color-blue3);
|
||||
border-color: var(--t-color-blue5);
|
||||
color: var(--t-color-blue);
|
||||
}
|
||||
|
||||
.red {
|
||||
background-color: var(--t-background-danger);
|
||||
border-color: var(--t-border-color-danger);
|
||||
color: var(--t-font-color-danger);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
declare const classNames: {
|
||||
readonly labelContainer: 'labelContainer';
|
||||
readonly blue: 'blue';
|
||||
readonly red: 'red';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type IconComponent } from '@ui/display';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { ThemeContext } from '@ui/theme-constants';
|
||||
import { clsx } from 'clsx';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import styles from './JsonNodeLabel.module.scss';
|
||||
|
||||
export const JsonNodeLabel = ({
|
||||
label,
|
||||
Icon,
|
||||
highlighting,
|
||||
}: {
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
highlighting?: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
styles.labelContainer,
|
||||
highlighting === 'blue' && styles.blue,
|
||||
highlighting === 'red' && styles.red,
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
color={
|
||||
highlighting === 'blue'
|
||||
? theme.color.blue
|
||||
: highlighting === 'red'
|
||||
? theme.font.color.danger
|
||||
: theme.font.color.tertiary
|
||||
}
|
||||
/>
|
||||
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
.text {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
color: var(--t-font-color-tertiary);
|
||||
display: inline-flex;
|
||||
height: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: var(--t-color-blue8);
|
||||
}
|
||||
|
||||
.red {
|
||||
color: var(--t-color-red8);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
declare const classNames: {
|
||||
readonly text: 'text';
|
||||
readonly blue: 'blue';
|
||||
readonly red: 'red';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import styles from './JsonNodeValue.module.scss';
|
||||
|
||||
export const JsonNodeValue = ({
|
||||
valueAsString,
|
||||
highlighting,
|
||||
}: {
|
||||
valueAsString: string;
|
||||
highlighting?: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { onNodeValueClick } = useJsonTreeContextOrThrow();
|
||||
|
||||
const handleClick = () => {
|
||||
onNodeValueClick?.(valueAsString);
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
styles.text,
|
||||
highlighting === 'blue' && styles.blue,
|
||||
highlighting === 'red' && styles.red,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{valueAsString}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number };
|
||||
|
||||
export type JsonTreeContextType = {
|
||||
getNodeHighlighting?: GetJsonNodeHighlighting;
|
||||
shouldExpandNodeInitially: (
|
||||
params: ShouldExpandNodeInitiallyProps,
|
||||
) => boolean;
|
||||
emptyStringLabel: string;
|
||||
emptyArrayLabel: string;
|
||||
emptyObjectLabel: string;
|
||||
arrowButtonCollapsedLabel: string;
|
||||
arrowButtonExpandedLabel: string;
|
||||
onNodeValueClick?: (valueAsString: string) => void;
|
||||
};
|
||||
|
||||
export const JsonTreeContext = createContext<JsonTreeContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { JsonTreeContext } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from '@ui/utilities/utils/isDefined';
|
||||
|
||||
export const useJsonTreeContextOrThrow = () => {
|
||||
const value = useContext(JsonTreeContext);
|
||||
|
||||
if (!isDefined(value)) {
|
||||
throw new Error(
|
||||
'useJsonTreeContextOrThrow must be used within a JsonTreeContextProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -7,4 +7,20 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export {};
|
||||
export { JsonArrayNode } from './components/JsonArrayNode';
|
||||
export { JsonNestedNode } from './components/JsonNestedNode';
|
||||
export { JsonNode } from './components/JsonNode';
|
||||
export { JsonObjectNode } from './components/JsonObjectNode';
|
||||
export { JsonTree } from './components/JsonTree';
|
||||
export { JsonTreeContextProvider } from './components/JsonTreeContextProvider';
|
||||
export { JsonValueNode } from './components/JsonValueNode';
|
||||
export type {
|
||||
ShouldExpandNodeInitiallyProps,
|
||||
JsonTreeContextType,
|
||||
} from './contexts/JsonTreeContext';
|
||||
export { JsonTreeContext } from './contexts/JsonTreeContext';
|
||||
export { useJsonTreeContextOrThrow } from './hooks/useJsonTreeContextOrThrow';
|
||||
export type { GetJsonNodeHighlighting } from './types/GetJsonNodeHighlighting';
|
||||
export type { JsonNodeHighlighting } from './types/JsonNodeHighlighting';
|
||||
export { isArray } from './utils/isArray';
|
||||
export { isTwoFirstDepths } from './utils/isTwoFirstDepths';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
|
||||
export type GetJsonNodeHighlighting = (
|
||||
keyPath: string,
|
||||
) => JsonNodeHighlighting | undefined;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { type ThemeColor } from '@ui/theme';
|
||||
|
||||
export type JsonNodeHighlighting =
|
||||
| Extract<ThemeColor, 'blue' | 'red'>
|
||||
| 'partial-blue';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { isArray as _isArray } from '@sniptt/guards';
|
||||
|
||||
export const isArray = (
|
||||
value: unknown,
|
||||
): value is unknown[] | readonly unknown[] => _isArray(value);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
|
||||
export const isTwoFirstDepths = ({ depth }: ShouldExpandNodeInitiallyProps) =>
|
||||
depth <= 1;
|
||||
Reference in New Issue
Block a user