Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 25 additions & 25 deletions __tests__/hostApi/chatReactRenderer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,44 +111,44 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
{/* Box A: contentAlignment, background+Rounded shape, border, fillMaxWidth */}
<Box
contentAlignment="topStart"
background={{ color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } }}
border={{ width: 1, color: 'textTertiary', shape: undefined }}
background={{ color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } }}
border={{ width: 1, color: 'fg.tertiary', shape: undefined }}
fillMaxWidth
>
<Text style="titleXL" color="textSecondary">
<Text style="title.medium.regular" color="fg.secondary">
Title
</Text>
</Box>
{/* Box B: background as plain ColorToken, width/height/minWidth/minHeight */}
<Box
contentAlignment="center"
background="backgroundPrimary"
background="bg.surface.main"
width={40}
height={40}
minWidth={20}
minHeight={20}
>
{/* Inner Box: Circle shape, fillMaxWidth + fillMaxHeight */}
<Box
background={{ color: 'backgroundTertiary', shape: { tag: 'Circle', value: undefined } }}
background={{ color: 'bg.surface.nested', shape: { tag: 'Circle', value: undefined } }}
fillMaxWidth
fillMaxHeight
/>
</Box>
{/* Row: verticalAlignment, horizontalArrangement, margin; covers bodyM/bodyS/caption + success/warning/error */}
<Row verticalAlignment="bottom" horizontalArrangement="spaceEvenly" margin={8}>
<Text style="bodyM" color="success">
<Text style="body.medium.regular" color="fg.success">
Item A
</Text>
<Spacer width={8} height={4} />
<Text style="bodyS" color="warning">
<Text style="body.small.regular" color="fg.warning">
Item B
</Text>
<Text style="caption" color="error">
<Text style="body.small.regular" color="fg.error">
Item C
</Text>
</Row>
<Text style="headline" color="textPrimary">
<Text style="headline.large" color="fg.primary">
Balance: 100 DOT
</Text>
{/* Spacer with fillMaxHeight */}
Expand Down Expand Up @@ -221,26 +221,26 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
const boxAMods = boxA.value.modifiers as RendererModifier[];
expect(boxAMods).toContainEqual({
tag: 'background',
value: { color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } },
value: { color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } },
});
expect(boxAMods).toContainEqual({
tag: 'border',
value: { width: 1, color: 'textTertiary', shape: undefined },
value: { width: 1, color: 'fg.tertiary', shape: undefined },
});
expect(boxAMods).toContainEqual({ tag: 'fillWidth', value: true });

// Box A > Text: titleXL + textSecondary
// Box A > Text: title.medium.regular + fg.secondary
const boxAText = boxA.value.children[0];
expect(boxAText.tag).toBe('Text');
expect(boxAText.value.props.style).toBe('titleXL');
expect(boxAText.value.props.color).toBe('textSecondary');
expect(boxAText.value.props.style).toBe('title.medium.regular');
expect(boxAText.value.props.color).toBe('fg.secondary');
expect(boxAText.value.children[0]).toEqual({ tag: 'String', value: 'Title' });

// Box B: contentAlignment, background as plain color token, width/height/minWidth/minHeight
expect(boxB.tag).toBe('Box');
expect(boxB.value.props.contentAlignment).toBe('center');
const boxBMods = boxB.value.modifiers as RendererModifier[];
expect(boxBMods).toContainEqual({ tag: 'background', value: { color: 'backgroundPrimary', shape: undefined } });
expect(boxBMods).toContainEqual({ tag: 'background', value: { color: 'bg.surface.main', shape: undefined } });
expect(boxBMods).toContainEqual({ tag: 'width', value: 40 });
expect(boxBMods).toContainEqual({ tag: 'height', value: 40 });
expect(boxBMods).toContainEqual({ tag: 'minWidth', value: 20 });
Expand All @@ -252,7 +252,7 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
const innerBoxMods = innerBox.value.modifiers as RendererModifier[];
expect(innerBoxMods).toContainEqual({
tag: 'background',
value: { color: 'backgroundTertiary', shape: { tag: 'Circle', value: undefined } },
value: { color: 'bg.surface.nested', shape: { tag: 'Circle', value: undefined } },
});
expect(innerBoxMods).toContainEqual({ tag: 'fillWidth', value: true });
expect(innerBoxMods).toContainEqual({ tag: 'fillHeight', value: true });
Expand All @@ -267,8 +267,8 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
// Row > Text: bodyM + success
const rowTextA = rowNode.value.children[0];
expect(rowTextA.tag).toBe('Text');
expect(rowTextA.value.props.style).toBe('bodyM');
expect(rowTextA.value.props.color).toBe('success');
expect(rowTextA.value.props.style).toBe('body.medium.regular');
expect(rowTextA.value.props.color).toBe('fg.success');
expect(rowTextA.value.children[0]).toEqual({ tag: 'String', value: 'Item A' });

// Row > Spacer: width + height modifiers
Expand All @@ -281,21 +281,21 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
// Row > Text: bodyS + warning
const rowTextB = rowNode.value.children[2];
expect(rowTextB.tag).toBe('Text');
expect(rowTextB.value.props.style).toBe('bodyS');
expect(rowTextB.value.props.color).toBe('warning');
expect(rowTextB.value.props.style).toBe('body.small.regular');
expect(rowTextB.value.props.color).toBe('fg.warning');
expect(rowTextB.value.children[0]).toEqual({ tag: 'String', value: 'Item B' });

// Row > Text: caption + error
const rowTextC = rowNode.value.children[3];
expect(rowTextC.tag).toBe('Text');
expect(rowTextC.value.props.style).toBe('caption');
expect(rowTextC.value.props.color).toBe('error');
expect(rowTextC.value.props.style).toBe('body.small.regular');
expect(rowTextC.value.props.color).toBe('fg.error');
expect(rowTextC.value.children[0]).toEqual({ tag: 'String', value: 'Item C' });

// Text: headline + textPrimary
// Text: headline.large + fg.primary
expect(headlineText.tag).toBe('Text');
expect(headlineText.value.props.style).toBe('headline');
expect(headlineText.value.props.color).toBe('textPrimary');
expect(headlineText.value.props.style).toBe('headline.large');
expect(headlineText.value.props.color).toBe('fg.primary');
expect(headlineText.value.children[0]).toEqual({ tag: 'String', value: 'Balance: 100 DOT' });

// Spacer: fillMaxHeight
Expand Down
26 changes: 16 additions & 10 deletions packages/host-api/src/protocol/v1/customRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ import { Option, Struct, Tuple, Vector, _void, bool, compact, str } from 'scale-
export const Size = compact;
export const Dimensions = Tuple(Size, Size, Option(Size), Option(Size));

export const TypographyStyle = Status('titleXL', 'headline', 'bodyM', 'bodyS', 'caption');
export const TypographyStyle = Status(
'headline.large',
'title.medium.regular',
'body.large.regular',
'body.medium.regular',
'body.small.regular',
);

export const ButtonVariant = Status('primary', 'secondary', 'text');

export const ColorToken = Status(
'textPrimary',
'textSecondary',
'textTertiary',
'backgroundPrimary',
'backgroundSecondary',
'backgroundTertiary',
'success',
'error',
'warning',
'fg.primary',
'fg.secondary',
'fg.tertiary',
'bg.surface.main',
'bg.surface.container',
'bg.surface.nested',
'fg.success',
'fg.error',
'fg.warning',
);

export const ContentAlignment = Status(
Expand Down
56 changes: 28 additions & 28 deletions packages/product-react-renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { registerChatMessageRenderer, Text } from '@novasamatech/product-react-r
chat.onCustomMessageRenderingRequest(
registerChatMessageRenderer(
() => undefined,
() => <Text style="headline">Hello from the product!</Text>,
() => <Text style="headline.large">Hello from the product!</Text>,
),
);
```
Expand All @@ -65,8 +65,8 @@ chat.onCustomMessageRenderingRequest(
raw => JSON.parse(new TextDecoder().decode(raw)) as BalancePayload,
({ payload }) => (
<Column>
<Text style="headline">{payload.amount}</Text>
<Text color="textSecondary">{payload.token}</Text>
<Text style="headline.large">{payload.amount}</Text>
<Text color="fg.secondary">{payload.token}</Text>
</Column>
),
),
Expand All @@ -85,7 +85,7 @@ function VoteWidget() {
const [votes, setVotes] = useState(0);
return (
<Column horizontalAlignment="center" padding={16}>
<Text style="headline">Votes: {votes}</Text>
<Text style="headline.large">Votes: {votes}</Text>
<Button text="Vote" variant="primary" onClick={() => setVotes(v => v + 1)} />
</Column>
);
Expand All @@ -110,7 +110,7 @@ chat.onCustomMessageRenderingRequest(
registerChatMessageRenderer(
() => undefined,
({ messageId, messageType }) => (
<Text color="textSecondary">
<Text color="fg.secondary">
[{messageType}] {messageId}
</Text>
),
Expand All @@ -132,10 +132,10 @@ All components accept the [shared layout props](#layout-props) in addition to th
| `color` | `ColorToken` | Text color |
| `children` | `ReactNode` | Text content or nested nodes |

**`TypographyStyle`**: `titleXL` · `headline` · `bodyM` · `bodyS` · `caption`
**`TypographyStyle`**: `headline.large` · `title.medium.regular` · `body.large.regular` · `body.medium.regular` · `body.small.regular`

```tsx
<Text style="headline" color="textPrimary">Balance: 42 DOT</Text>
<Text style="headline.large" color="fg.primary">Balance: 42 DOT</Text>
```

### `<Button>`
Expand Down Expand Up @@ -217,7 +217,7 @@ Stacks children vertically.

```tsx
<Column horizontalAlignment="center" verticalArrangement="spaceBetween" padding={16}>
<Text style="headline">Title</Text>
<Text style="headline.large">Title</Text>
<Button text="OK" onClick={handleOk} />
</Column>
```
Expand All @@ -236,7 +236,7 @@ Stacks children horizontally.
```tsx
<Row verticalAlignment="center" horizontalArrangement="spaceBetween">
<Text>Label</Text>
<Text color="textSecondary">Value</Text>
<Text color="fg.secondary">Value</Text>
</Row>
```

Expand All @@ -251,7 +251,7 @@ Single-child container with optional content alignment.
**`ContentAlignment`**: `topStart` · `topCenter` · `topEnd` · `centerStart` · `center` · `centerEnd` · `bottomStart` · `bottomCenter` · `bottomEnd`

```tsx
<Box contentAlignment="center" background="backgroundSecondary" padding={8}>
<Box contentAlignment="center" background="bg.surface.container" padding={8}>
<Text>Centered</Text>
</Box>
```
Expand Down Expand Up @@ -300,19 +300,19 @@ Every component accepts these props to control sizing, spacing, and appearance.

```tsx
// Plain color
<Box background="backgroundSecondary" />
<Box background="bg.surface.container" />

// Color + shape
<Box background={{ color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } }} />
<Box background={{ color: 'backgroundTertiary', shape: { tag: 'Circle' } }} />
<Box background={{ color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } }} />
<Box background={{ color: 'bg.surface.nested', shape: { tag: 'Circle' } }} />
```

### Border

```tsx
<Box border={{ width: 1, color: 'textTertiary' }} />
<Box border={{ width: 1, color: 'fg.tertiary' }} />
// With a rounded corner
<Box border={{ width: 1, color: 'success', shape: { tag: 'Rounded', value: 4 } }} />
<Box border={{ width: 1, color: 'fg.success', shape: { tag: 'Rounded', value: 4 } }} />
```

---
Expand All @@ -321,15 +321,15 @@ Every component accepts these props to control sizing, spacing, and appearance.

| Token | Description |
|------------------------|-----------------------------|
| `textPrimary` | Primary text |
| `textSecondary` | Secondary / supporting text |
| `textTertiary` | Tertiary / hint text |
| `backgroundPrimary` | Primary surface |
| `backgroundSecondary` | Secondary surface |
| `backgroundTertiary` | Tertiary surface |
| `success` | Positive / success state |
| `warning` | Warning state |
| `error` | Error / destructive state |
| `fg.primary` | Primary text |
| `fg.secondary` | Secondary / supporting text |
| `fg.tertiary` | Tertiary / hint text |
| `bg.surface.main` | Primary surface |
| `bg.surface.container` | Secondary surface |
| `bg.surface.nested` | Tertiary surface |
| `fg.success` | Positive / success state |
| `fg.warning` | Warning state |
| `fg.error` | Error / destructive state |

---

Expand Down Expand Up @@ -367,7 +367,7 @@ const renderer = createRenderer({
// Mount the initial tree.
renderer.mount(
<Column>
<Text style="headline">Hello</Text>
<Text style="headline.large">Hello</Text>
<Button text="OK" onClick={() => console.log('clicked')} />
</Column>,
);
Expand All @@ -382,10 +382,10 @@ renderer.unmount();

```tsx
// First render
renderer.mount(<Text style="headline">Loading…</Text>);
renderer.mount(<Text style="headline.large">Loading…</Text>);

// Later — update in place
renderer.mount(<Text style="headline">Done!</Text>);
renderer.mount(<Text style="headline.large">Done!</Text>);
```

### Manual integration with `onCustomMessageRenderingRequest`
Expand All @@ -398,7 +398,7 @@ import { createRenderer, Text } from '@novasamatech/product-react-renderer';
chat.onCustomMessageRenderingRequest(({ messageId, messageType, payload, subscribeActions }, render) => {
const renderer = createRenderer({ onRender: render, subscribeActions });

renderer.mount(<Text style="headline">{messageType}</Text>);
renderer.mount(<Text style="headline.large">{messageType}</Text>);

// Return the cleanup callback.
return () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/product-react-renderer/src/components.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ describe('custom components', () => {

describe('Text', () => {
it('serializes style and color props', async () => {
const { node } = await mount(<Text style="headline" color="textPrimary" />);
const { node } = await mount(<Text style="headline.large" color="fg.primary" />);
expect(node.tag).toBe('Text');
expect(node.value.props.style).toBe('headline');
expect(node.value.props.color).toBe('textPrimary');
expect(node.value.props.style).toBe('headline.large');
expect(node.value.props.color).toBe('fg.primary');
});

it('renders text content as a String child', async () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/product-react-renderer/src/renderer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('createRenderer', () => {
const renderer = createRenderer({ onRender, subscribeActions });
await act(async () => {
renderer.mount(
<Text style="headline" color="textPrimary">
<Text style="headline.large" color="fg.primary">
Hello
</Text>,
);
Expand All @@ -56,8 +56,8 @@ describe('createRenderer', () => {
expect(onRender).toHaveBeenCalledOnce();
const node = onRender.mock.calls[0]![0];
expect(node.tag).toBe('Text');
expect(node.value.props.style).toBe('headline');
expect(node.value.props.color).toBe('textPrimary');
expect(node.value.props.style).toBe('headline.large');
expect(node.value.props.color).toBe('fg.primary');
expect(node.value.children[0]).toEqual({ tag: 'String', value: 'Hello' });
});

Expand All @@ -82,7 +82,7 @@ describe('createRenderer', () => {
await act(async () => {
renderer.mount(
<Column padding={16} horizontalAlignment="center">
<Text style="bodyM">Label</Text>
<Text style="body.medium.regular">Label</Text>
<Spacer />
</Column>,
);
Expand Down