Skip to content

Commit 9e60c2b

Browse files
authored
feat: design system tokens (#172)
1 parent c5c628d commit 9e60c2b

5 files changed

Lines changed: 76 additions & 70 deletions

File tree

__tests__/hostApi/chatReactRenderer.spec.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -111,44 +111,44 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
111111
{/* Box A: contentAlignment, background+Rounded shape, border, fillMaxWidth */}
112112
<Box
113113
contentAlignment="topStart"
114-
background={{ color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } }}
115-
border={{ width: 1, color: 'textTertiary', shape: undefined }}
114+
background={{ color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } }}
115+
border={{ width: 1, color: 'fg.tertiary', shape: undefined }}
116116
fillMaxWidth
117117
>
118-
<Text style="titleXL" color="textSecondary">
118+
<Text style="title.medium.regular" color="fg.secondary">
119119
Title
120120
</Text>
121121
</Box>
122122
{/* Box B: background as plain ColorToken, width/height/minWidth/minHeight */}
123123
<Box
124124
contentAlignment="center"
125-
background="backgroundPrimary"
125+
background="bg.surface.main"
126126
width={40}
127127
height={40}
128128
minWidth={20}
129129
minHeight={20}
130130
>
131131
{/* Inner Box: Circle shape, fillMaxWidth + fillMaxHeight */}
132132
<Box
133-
background={{ color: 'backgroundTertiary', shape: { tag: 'Circle', value: undefined } }}
133+
background={{ color: 'bg.surface.nested', shape: { tag: 'Circle', value: undefined } }}
134134
fillMaxWidth
135135
fillMaxHeight
136136
/>
137137
</Box>
138138
{/* Row: verticalAlignment, horizontalArrangement, margin; covers bodyM/bodyS/caption + success/warning/error */}
139139
<Row verticalAlignment="bottom" horizontalArrangement="spaceEvenly" margin={8}>
140-
<Text style="bodyM" color="success">
140+
<Text style="body.medium.regular" color="fg.success">
141141
Item A
142142
</Text>
143143
<Spacer width={8} height={4} />
144-
<Text style="bodyS" color="warning">
144+
<Text style="body.small.regular" color="fg.warning">
145145
Item B
146146
</Text>
147-
<Text style="caption" color="error">
147+
<Text style="body.small.regular" color="fg.error">
148148
Item C
149149
</Text>
150150
</Row>
151-
<Text style="headline" color="textPrimary">
151+
<Text style="headline.large" color="fg.primary">
152152
Balance: 100 DOT
153153
</Text>
154154
{/* Spacer with fillMaxHeight */}
@@ -221,26 +221,26 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
221221
const boxAMods = boxA.value.modifiers as RendererModifier[];
222222
expect(boxAMods).toContainEqual({
223223
tag: 'background',
224-
value: { color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } },
224+
value: { color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } },
225225
});
226226
expect(boxAMods).toContainEqual({
227227
tag: 'border',
228-
value: { width: 1, color: 'textTertiary', shape: undefined },
228+
value: { width: 1, color: 'fg.tertiary', shape: undefined },
229229
});
230230
expect(boxAMods).toContainEqual({ tag: 'fillWidth', value: true });
231231

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

239239
// Box B: contentAlignment, background as plain color token, width/height/minWidth/minHeight
240240
expect(boxB.tag).toBe('Box');
241241
expect(boxB.value.props.contentAlignment).toBe('center');
242242
const boxBMods = boxB.value.modifiers as RendererModifier[];
243-
expect(boxBMods).toContainEqual({ tag: 'background', value: { color: 'backgroundPrimary', shape: undefined } });
243+
expect(boxBMods).toContainEqual({ tag: 'background', value: { color: 'bg.surface.main', shape: undefined } });
244244
expect(boxBMods).toContainEqual({ tag: 'width', value: 40 });
245245
expect(boxBMods).toContainEqual({ tag: 'height', value: 40 });
246246
expect(boxBMods).toContainEqual({ tag: 'minWidth', value: 20 });
@@ -252,7 +252,7 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
252252
const innerBoxMods = innerBox.value.modifiers as RendererModifier[];
253253
expect(innerBoxMods).toContainEqual({
254254
tag: 'background',
255-
value: { color: 'backgroundTertiary', shape: { tag: 'Circle', value: undefined } },
255+
value: { color: 'bg.surface.nested', shape: { tag: 'Circle', value: undefined } },
256256
});
257257
expect(innerBoxMods).toContainEqual({ tag: 'fillWidth', value: true });
258258
expect(innerBoxMods).toContainEqual({ tag: 'fillHeight', value: true });
@@ -267,8 +267,8 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
267267
// Row > Text: bodyM + success
268268
const rowTextA = rowNode.value.children[0];
269269
expect(rowTextA.tag).toBe('Text');
270-
expect(rowTextA.value.props.style).toBe('bodyM');
271-
expect(rowTextA.value.props.color).toBe('success');
270+
expect(rowTextA.value.props.style).toBe('body.medium.regular');
271+
expect(rowTextA.value.props.color).toBe('fg.success');
272272
expect(rowTextA.value.children[0]).toEqual({ tag: 'String', value: 'Item A' });
273273

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

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

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

301301
// Spacer: fillMaxHeight

packages/host-api/src/protocol/v1/customRenderer.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,26 @@ import { Option, Struct, Tuple, Vector, _void, bool, compact, str } from 'scale-
55
export const Size = compact;
66
export const Dimensions = Tuple(Size, Size, Option(Size), Option(Size));
77

8-
export const TypographyStyle = Status('titleXL', 'headline', 'bodyM', 'bodyS', 'caption');
8+
export const TypographyStyle = Status(
9+
'headline.large',
10+
'title.medium.regular',
11+
'body.large.regular',
12+
'body.medium.regular',
13+
'body.small.regular',
14+
);
915

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

1218
export const ColorToken = Status(
13-
'textPrimary',
14-
'textSecondary',
15-
'textTertiary',
16-
'backgroundPrimary',
17-
'backgroundSecondary',
18-
'backgroundTertiary',
19-
'success',
20-
'error',
21-
'warning',
19+
'fg.primary',
20+
'fg.secondary',
21+
'fg.tertiary',
22+
'bg.surface.main',
23+
'bg.surface.container',
24+
'bg.surface.nested',
25+
'fg.success',
26+
'fg.error',
27+
'fg.warning',
2228
);
2329

2430
export const ContentAlignment = Status(

packages/product-react-renderer/README.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { registerChatMessageRenderer, Text } from '@novasamatech/product-react-r
4646
chat.onCustomMessageRenderingRequest(
4747
registerChatMessageRenderer(
4848
() => undefined,
49-
() => <Text style="headline">Hello from the product!</Text>,
49+
() => <Text style="headline.large">Hello from the product!</Text>,
5050
),
5151
);
5252
```
@@ -65,8 +65,8 @@ chat.onCustomMessageRenderingRequest(
6565
raw => JSON.parse(new TextDecoder().decode(raw)) as BalancePayload,
6666
({ payload }) => (
6767
<Column>
68-
<Text style="headline">{payload.amount}</Text>
69-
<Text color="textSecondary">{payload.token}</Text>
68+
<Text style="headline.large">{payload.amount}</Text>
69+
<Text color="fg.secondary">{payload.token}</Text>
7070
</Column>
7171
),
7272
),
@@ -85,7 +85,7 @@ function VoteWidget() {
8585
const [votes, setVotes] = useState(0);
8686
return (
8787
<Column horizontalAlignment="center" padding={16}>
88-
<Text style="headline">Votes: {votes}</Text>
88+
<Text style="headline.large">Votes: {votes}</Text>
8989
<Button text="Vote" variant="primary" onClick={() => setVotes(v => v + 1)} />
9090
</Column>
9191
);
@@ -110,7 +110,7 @@ chat.onCustomMessageRenderingRequest(
110110
registerChatMessageRenderer(
111111
() => undefined,
112112
({ messageId, messageType }) => (
113-
<Text color="textSecondary">
113+
<Text color="fg.secondary">
114114
[{messageType}] {messageId}
115115
</Text>
116116
),
@@ -132,10 +132,10 @@ All components accept the [shared layout props](#layout-props) in addition to th
132132
| `color` | `ColorToken` | Text color |
133133
| `children` | `ReactNode` | Text content or nested nodes |
134134

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

137137
```tsx
138-
<Text style="headline" color="textPrimary">Balance: 42 DOT</Text>
138+
<Text style="headline.large" color="fg.primary">Balance: 42 DOT</Text>
139139
```
140140

141141
### `<Button>`
@@ -217,7 +217,7 @@ Stacks children vertically.
217217

218218
```tsx
219219
<Column horizontalAlignment="center" verticalArrangement="spaceBetween" padding={16}>
220-
<Text style="headline">Title</Text>
220+
<Text style="headline.large">Title</Text>
221221
<Button text="OK" onClick={handleOk} />
222222
</Column>
223223
```
@@ -236,7 +236,7 @@ Stacks children horizontally.
236236
```tsx
237237
<Row verticalAlignment="center" horizontalArrangement="spaceBetween">
238238
<Text>Label</Text>
239-
<Text color="textSecondary">Value</Text>
239+
<Text color="fg.secondary">Value</Text>
240240
</Row>
241241
```
242242

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

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

301301
```tsx
302302
// Plain color
303-
<Box background="backgroundSecondary" />
303+
<Box background="bg.surface.container" />
304304

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

310310
### Border
311311

312312
```tsx
313-
<Box border={{ width: 1, color: 'textTertiary' }} />
313+
<Box border={{ width: 1, color: 'fg.tertiary' }} />
314314
// With a rounded corner
315-
<Box border={{ width: 1, color: 'success', shape: { tag: 'Rounded', value: 4 } }} />
315+
<Box border={{ width: 1, color: 'fg.success', shape: { tag: 'Rounded', value: 4 } }} />
316316
```
317317

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

322322
| Token | Description |
323323
|------------------------|-----------------------------|
324-
| `textPrimary` | Primary text |
325-
| `textSecondary` | Secondary / supporting text |
326-
| `textTertiary` | Tertiary / hint text |
327-
| `backgroundPrimary` | Primary surface |
328-
| `backgroundSecondary` | Secondary surface |
329-
| `backgroundTertiary` | Tertiary surface |
330-
| `success` | Positive / success state |
331-
| `warning` | Warning state |
332-
| `error` | Error / destructive state |
324+
| `fg.primary` | Primary text |
325+
| `fg.secondary` | Secondary / supporting text |
326+
| `fg.tertiary` | Tertiary / hint text |
327+
| `bg.surface.main` | Primary surface |
328+
| `bg.surface.container` | Secondary surface |
329+
| `bg.surface.nested` | Tertiary surface |
330+
| `fg.success` | Positive / success state |
331+
| `fg.warning` | Warning state |
332+
| `fg.error` | Error / destructive state |
333333

334334
---
335335

@@ -367,7 +367,7 @@ const renderer = createRenderer({
367367
// Mount the initial tree.
368368
renderer.mount(
369369
<Column>
370-
<Text style="headline">Hello</Text>
370+
<Text style="headline.large">Hello</Text>
371371
<Button text="OK" onClick={() => console.log('clicked')} />
372372
</Column>,
373373
);
@@ -382,10 +382,10 @@ renderer.unmount();
382382

383383
```tsx
384384
// First render
385-
renderer.mount(<Text style="headline">Loading…</Text>);
385+
renderer.mount(<Text style="headline.large">Loading…</Text>);
386386

387387
// Later — update in place
388-
renderer.mount(<Text style="headline">Done!</Text>);
388+
renderer.mount(<Text style="headline.large">Done!</Text>);
389389
```
390390

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

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

403403
// Return the cleanup callback.
404404
return () => {

packages/product-react-renderer/src/components.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ describe('custom components', () => {
117117

118118
describe('Text', () => {
119119
it('serializes style and color props', async () => {
120-
const { node } = await mount(<Text style="headline" color="textPrimary" />);
120+
const { node } = await mount(<Text style="headline.large" color="fg.primary" />);
121121
expect(node.tag).toBe('Text');
122-
expect(node.value.props.style).toBe('headline');
123-
expect(node.value.props.color).toBe('textPrimary');
122+
expect(node.value.props.style).toBe('headline.large');
123+
expect(node.value.props.color).toBe('fg.primary');
124124
});
125125

126126
it('renders text content as a String child', async () => {

packages/product-react-renderer/src/renderer.spec.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('createRenderer', () => {
4747
const renderer = createRenderer({ onRender, subscribeActions });
4848
await act(async () => {
4949
renderer.mount(
50-
<Text style="headline" color="textPrimary">
50+
<Text style="headline.large" color="fg.primary">
5151
Hello
5252
</Text>,
5353
);
@@ -56,8 +56,8 @@ describe('createRenderer', () => {
5656
expect(onRender).toHaveBeenCalledOnce();
5757
const node = onRender.mock.calls[0]![0];
5858
expect(node.tag).toBe('Text');
59-
expect(node.value.props.style).toBe('headline');
60-
expect(node.value.props.color).toBe('textPrimary');
59+
expect(node.value.props.style).toBe('headline.large');
60+
expect(node.value.props.color).toBe('fg.primary');
6161
expect(node.value.children[0]).toEqual({ tag: 'String', value: 'Hello' });
6262
});
6363

@@ -82,7 +82,7 @@ describe('createRenderer', () => {
8282
await act(async () => {
8383
renderer.mount(
8484
<Column padding={16} horizontalAlignment="center">
85-
<Text style="bodyM">Label</Text>
85+
<Text style="body.medium.regular">Label</Text>
8686
<Spacer />
8787
</Column>,
8888
);

0 commit comments

Comments
 (0)