@ankhorage/zora 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +44 -1
  3. package/dist/components/card/Card.d.ts.map +1 -1
  4. package/dist/components/card/Card.js +2 -1
  5. package/dist/components/card/Card.js.map +1 -1
  6. package/dist/components/checkbox/CheckboxGroup.d.ts.map +1 -1
  7. package/dist/components/checkbox/CheckboxGroup.js +2 -1
  8. package/dist/components/checkbox/CheckboxGroup.js.map +1 -1
  9. package/dist/components/drawer/Drawer.d.ts.map +1 -1
  10. package/dist/components/drawer/Drawer.js +2 -1
  11. package/dist/components/drawer/Drawer.js.map +1 -1
  12. package/dist/components/form/FormError.d.ts.map +1 -1
  13. package/dist/components/form/FormError.js +3 -2
  14. package/dist/components/form/FormError.js.map +1 -1
  15. package/dist/components/form/FormField.d.ts.map +1 -1
  16. package/dist/components/form/FormField.js +2 -1
  17. package/dist/components/form/FormField.js.map +1 -1
  18. package/dist/components/input/Input.js +3 -3
  19. package/dist/components/input/Input.js.map +1 -1
  20. package/dist/components/input/types.d.ts +4 -4
  21. package/dist/components/input/types.d.ts.map +1 -1
  22. package/dist/components/input/types.js.map +1 -1
  23. package/dist/components/modal/Modal.d.ts.map +1 -1
  24. package/dist/components/modal/Modal.js +2 -1
  25. package/dist/components/modal/Modal.js.map +1 -1
  26. package/dist/components/radio/RadioGroup.d.ts.map +1 -1
  27. package/dist/components/radio/RadioGroup.js +2 -1
  28. package/dist/components/radio/RadioGroup.js.map +1 -1
  29. package/dist/components/tabs/Tabs.d.ts.map +1 -1
  30. package/dist/components/tabs/Tabs.js +3 -2
  31. package/dist/components/tabs/Tabs.js.map +1 -1
  32. package/dist/components/text/Text.d.ts +4 -0
  33. package/dist/components/text/Text.d.ts.map +1 -0
  34. package/dist/components/text/Text.js +47 -0
  35. package/dist/components/text/Text.js.map +1 -0
  36. package/dist/components/text/index.d.ts +3 -0
  37. package/dist/components/text/index.d.ts.map +1 -0
  38. package/dist/components/text/index.js +2 -0
  39. package/dist/components/text/index.js.map +1 -0
  40. package/dist/components/text/resolveTextRecipe.d.ts +15 -0
  41. package/dist/components/text/resolveTextRecipe.d.ts.map +1 -0
  42. package/dist/components/text/resolveTextRecipe.js +110 -0
  43. package/dist/components/text/resolveTextRecipe.js.map +1 -0
  44. package/dist/components/text/types.d.ts +26 -0
  45. package/dist/components/text/types.d.ts.map +1 -0
  46. package/dist/components/text/types.js +2 -0
  47. package/dist/components/text/types.js.map +1 -0
  48. package/dist/components/textarea/Textarea.js +3 -3
  49. package/dist/components/textarea/Textarea.js.map +1 -1
  50. package/dist/components/textarea/types.d.ts +4 -4
  51. package/dist/components/textarea/types.d.ts.map +1 -1
  52. package/dist/components/textarea/types.js.map +1 -1
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/layout/page-header/PageHeader.d.ts.map +1 -1
  58. package/dist/layout/page-header/PageHeader.js +2 -1
  59. package/dist/layout/page-header/PageHeader.js.map +1 -1
  60. package/dist/patterns/collection-editor/CollectionEditor.d.ts.map +1 -1
  61. package/dist/patterns/collection-editor/CollectionEditor.js +2 -1
  62. package/dist/patterns/collection-editor/CollectionEditor.js.map +1 -1
  63. package/dist/patterns/form-field/FormField.d.ts.map +1 -1
  64. package/dist/patterns/form-field/FormField.js +2 -1
  65. package/dist/patterns/form-field/FormField.js.map +1 -1
  66. package/dist/patterns/form-field/index.d.ts +0 -1
  67. package/dist/patterns/form-field/index.d.ts.map +1 -1
  68. package/dist/patterns/form-field/index.js.map +1 -1
  69. package/dist/patterns/section-header/SectionHeader.d.ts.map +1 -1
  70. package/dist/patterns/section-header/SectionHeader.js +2 -1
  71. package/dist/patterns/section-header/SectionHeader.js.map +1 -1
  72. package/dist/patterns/settings-row/SettingsRow.d.ts.map +1 -1
  73. package/dist/patterns/settings-row/SettingsRow.js +2 -1
  74. package/dist/patterns/settings-row/SettingsRow.js.map +1 -1
  75. package/dist/patterns/tile-grid/PaletteItem.d.ts.map +1 -1
  76. package/dist/patterns/tile-grid/PaletteItem.js +2 -1
  77. package/dist/patterns/tile-grid/PaletteItem.js.map +1 -1
  78. package/package.json +2 -2
  79. package/src/components/card/Card.tsx +2 -1
  80. package/src/components/checkbox/CheckboxGroup.tsx +2 -1
  81. package/src/components/drawer/Drawer.tsx +2 -1
  82. package/src/components/form/FormError.tsx +3 -2
  83. package/src/components/form/FormField.tsx +2 -1
  84. package/src/components/input/Input.tsx +5 -5
  85. package/src/components/input/types.ts +4 -4
  86. package/src/components/modal/Modal.tsx +2 -1
  87. package/src/components/radio/RadioGroup.tsx +2 -1
  88. package/src/components/tabs/Tabs.tsx +3 -6
  89. package/src/components/text/Text.tsx +93 -0
  90. package/src/components/text/index.ts +2 -0
  91. package/src/components/text/resolveTextRecipe.test.ts +333 -0
  92. package/src/components/text/resolveTextRecipe.ts +169 -0
  93. package/src/components/text/types.ts +38 -0
  94. package/src/components/textarea/Textarea.tsx +5 -5
  95. package/src/components/textarea/types.ts +4 -4
  96. package/src/index.ts +2 -0
  97. package/src/layout/page-header/PageHeader.tsx +2 -1
  98. package/src/patterns/collection-editor/CollectionEditor.tsx +2 -1
  99. package/src/patterns/form-field/FormField.tsx +2 -1
  100. package/src/patterns/form-field/index.ts +0 -1
  101. package/src/patterns/section-header/SectionHeader.tsx +2 -1
  102. package/src/patterns/settings-row/SettingsRow.tsx +2 -1
  103. package/src/patterns/tile-grid/PaletteItem.tsx +2 -1
@@ -1,6 +1,7 @@
1
- import { Box, Text, useTheme } from '@ankhorage/surface';
1
+ import { Box, useTheme } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
+ import { Text } from '../text';
4
5
  import type { FormErrorProps } from './types';
5
6
 
6
7
  export function FormError({ error, testID }: FormErrorProps) {
@@ -12,7 +13,7 @@ export function FormError({ error, testID }: FormErrorProps) {
12
13
 
13
14
  return (
14
15
  <Box borderColor={theme.colors.error} borderWidth={1} p="s" radius="m" testID={testID}>
15
- <Text color={theme.colors.error} variant="bodySmall">
16
+ <Text tone="danger" variant="bodySmall">
16
17
  {error}
17
18
  </Text>
18
19
  </Box>
@@ -1,7 +1,8 @@
1
- import { Field, Stack, Text } from '@ankhorage/surface';
1
+ import { Field, Stack } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
4
  import { Input } from '../input';
5
+ import { Text } from '../text';
5
6
  import type { FormFieldConfig, FormFieldControlProps, FormFieldProps } from './types';
6
7
  import { hasRequiredRule } from './validation';
7
8
 
@@ -1,20 +1,20 @@
1
- import { Icon, TextInput as SurfaceTextInput, useTheme } from '@ankhorage/surface';
1
+ import * as Surface from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
4
  import { resolveIconSize } from '../../internal/recipes';
5
5
  import type { InputProps } from './types';
6
6
 
7
7
  export function Input({ size = 'l', leadingIcon, trailingIcon, ...props }: InputProps) {
8
- const { theme } = useTheme();
8
+ const { theme } = Surface.useTheme();
9
9
  const iconSize = resolveIconSize(size);
10
10
  const iconColor = theme.semantics.content.muted;
11
11
 
12
12
  return (
13
- <SurfaceTextInput
13
+ <Surface.TextInput
14
14
  {...props}
15
15
  leadingAccessory={
16
16
  leadingIcon ? (
17
- <Icon
17
+ <Surface.Icon
18
18
  color={iconColor}
19
19
  name={leadingIcon.name}
20
20
  provider={leadingIcon.provider}
@@ -25,7 +25,7 @@ export function Input({ size = 'l', leadingIcon, trailingIcon, ...props }: Input
25
25
  size={size}
26
26
  trailingAccessory={
27
27
  trailingIcon ? (
28
- <Icon
28
+ <Surface.Icon
29
29
  color={iconColor}
30
30
  name={trailingIcon.name}
31
31
  provider={trailingIcon.provider}
@@ -1,12 +1,12 @@
1
- import type { ButtonIconSpec, TextInputProps as SurfaceTextInputProps } from '@ankhorage/surface';
1
+ import type * as Surface from '@ankhorage/surface';
2
2
 
3
3
  import type { ZoraControlSize } from '../../internal/recipes';
4
4
 
5
5
  export interface InputProps extends Omit<
6
- SurfaceTextInputProps,
6
+ Surface.TextInputProps,
7
7
  'leadingAccessory' | 'size' | 'trailingAccessory'
8
8
  > {
9
9
  size?: ZoraControlSize;
10
- leadingIcon?: ButtonIconSpec;
11
- trailingIcon?: ButtonIconSpec;
10
+ leadingIcon?: Surface.ButtonIconSpec;
11
+ trailingIcon?: Surface.ButtonIconSpec;
12
12
  }
@@ -1,7 +1,8 @@
1
- import { Box, Heading, Modal as SurfaceModal, Stack, Text } from '@ankhorage/surface';
1
+ import { Box, Heading, Modal as SurfaceModal, Stack } from '@ankhorage/surface';
2
2
  import React, { useCallback, useEffect, useRef } from 'react';
3
3
 
4
4
  import { resolveDialogWidth } from '../../internal/recipes';
5
+ import { Text } from '../text';
5
6
  import type { ModalProps } from './types';
6
7
 
7
8
  function useStableCallback(callback: (() => void) | undefined): (() => void) | undefined {
@@ -1,7 +1,8 @@
1
- import { Radio, Stack, Text } from '@ankhorage/surface';
1
+ import { Radio, Stack } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
  import { View } from 'react-native';
4
4
 
5
+ import { Text } from '../text';
5
6
  import type { RadioGroupOption, RadioGroupProps } from './types';
6
7
 
7
8
  export function RadioGroup<TValue extends string>({
@@ -1,7 +1,8 @@
1
- import { Box, Stack, Text, useTheme } from '@ankhorage/surface';
1
+ import { Box, Stack, useTheme } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
4
  import { Button } from '../button';
5
+ import { Text } from '../text';
5
6
  import type { TabItem, TabsProps } from './types';
6
7
 
7
8
  export function Tabs<TValue extends string = string>({
@@ -74,11 +75,7 @@ export function Tabs<TValue extends string = string>({
74
75
  leadingIcon={item.icon}
75
76
  testID={item.testID}
76
77
  >
77
- <Text
78
- color={isActive ? theme.colors.primary : undefined}
79
- tone={isActive ? undefined : 'muted'}
80
- weight={isActive ? 'semiBold' : 'regular'}
81
- >
78
+ <Text tone={isActive ? 'primary' : 'muted'} weight={isActive ? 'semiBold' : 'regular'}>
82
79
  {item.label}
83
80
  </Text>
84
81
  {item.badge}
@@ -0,0 +1,93 @@
1
+ import { resolveResponsive, useResponsiveRuntime, useTranslationContext } from '@ankhorage/surface';
2
+ import React from 'react';
3
+ import { Text as ReactNativeText } from 'react-native';
4
+
5
+ import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { resolveTextStyle } from './resolveTextRecipe';
7
+ import type { TextProps } from './types';
8
+
9
+ function resolveTextContent({
10
+ children,
11
+ text,
12
+ i18nKey,
13
+ translate,
14
+ }: {
15
+ children: TextProps['children'];
16
+ text: TextProps['text'];
17
+ i18nKey: TextProps['i18nKey'];
18
+ translate: (key: string) => string;
19
+ }): React.ReactNode {
20
+ if (children !== undefined) {
21
+ return children;
22
+ }
23
+
24
+ if (text !== undefined) {
25
+ return text;
26
+ }
27
+
28
+ if (!i18nKey) {
29
+ return null;
30
+ }
31
+
32
+ try {
33
+ const translated = translate(i18nKey);
34
+ return translated && translated !== i18nKey ? translated : i18nKey;
35
+ } catch (error) {
36
+ console.warn('[ZoraText] Translation error:', error);
37
+ return i18nKey;
38
+ }
39
+ }
40
+
41
+ export function Text({
42
+ children,
43
+ text,
44
+ i18nKey,
45
+ variant = 'body',
46
+ tone = 'default',
47
+ align,
48
+ weight,
49
+ italic = false,
50
+ numberOfLines,
51
+ ellipsizeMode,
52
+ selectable,
53
+ accessibilityLabel,
54
+ accessibilityHint,
55
+ accessibilityRole,
56
+ nativeID,
57
+ testID,
58
+ }: TextProps) {
59
+ const { theme } = useZoraTheme();
60
+ const { breakpoint } = useResponsiveRuntime();
61
+ const { t } = useTranslationContext();
62
+ const content = resolveTextContent({ children, text, i18nKey, translate: t });
63
+ const resolvedVariant = resolveResponsive(variant, breakpoint) ?? 'body';
64
+ const resolvedStyle = resolveTextStyle({
65
+ theme,
66
+ breakpoint,
67
+ variant: resolvedVariant,
68
+ tone,
69
+ align,
70
+ weight,
71
+ italic,
72
+ });
73
+
74
+ if (content === null || content === undefined) {
75
+ return null;
76
+ }
77
+
78
+ return (
79
+ <ReactNativeText
80
+ accessibilityHint={accessibilityHint}
81
+ accessibilityLabel={accessibilityLabel}
82
+ accessibilityRole={accessibilityRole}
83
+ ellipsizeMode={ellipsizeMode}
84
+ nativeID={nativeID}
85
+ numberOfLines={numberOfLines}
86
+ selectable={selectable}
87
+ testID={testID}
88
+ style={resolvedStyle}
89
+ >
90
+ {content}
91
+ </ReactNativeText>
92
+ );
93
+ }
@@ -0,0 +1,2 @@
1
+ export { Text } from './Text';
2
+ export type { TextAlign, TextProps, TextTone, TextVariant, TextWeight } from './types';
@@ -0,0 +1,333 @@
1
+ import type {
2
+ AnkhTheme,
3
+ Breakpoint,
4
+ FontWeight,
5
+ Responsive,
6
+ RoleSemantics,
7
+ } from '@ankhorage/surface';
8
+ import { describe, expect, mock, test } from 'bun:test';
9
+
10
+ const breakpointOrder: readonly Breakpoint[] = ['base', 'sm', 'md', 'lg', 'xl'];
11
+
12
+ function isResponsiveRecord<T>(value: Responsive<T>): value is Partial<Record<Breakpoint, T>> {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ function resolveResponsiveMock<T>(
17
+ value: Responsive<T> | undefined,
18
+ breakpoint: Breakpoint,
19
+ ): T | undefined {
20
+ if (value === undefined) return undefined;
21
+ if (!isResponsiveRecord(value)) return value;
22
+
23
+ const activeIndex = breakpointOrder.indexOf(breakpoint);
24
+ for (let i = activeIndex; i >= 0; i -= 1) {
25
+ const key = breakpointOrder[i];
26
+ if (!key) continue;
27
+ const candidate = value[key];
28
+ if (candidate !== undefined) return candidate;
29
+ }
30
+
31
+ return undefined;
32
+ }
33
+
34
+ await mock.module('@ankhorage/surface', () => ({
35
+ resolveResponsive: resolveResponsiveMock,
36
+ }));
37
+
38
+ const { resolveTextStyle } = await import('./resolveTextRecipe');
39
+
40
+ const emptyFonts: Record<FontWeight, string | undefined> = {
41
+ '100': undefined,
42
+ '200': undefined,
43
+ '300': undefined,
44
+ '400': undefined,
45
+ '500': undefined,
46
+ '600': undefined,
47
+ '700': undefined,
48
+ '800': undefined,
49
+ '900': undefined,
50
+ bold: undefined,
51
+ normal: undefined,
52
+ };
53
+
54
+ function createRole(base: string): RoleSemantics {
55
+ return {
56
+ base,
57
+ hover: `${base}-hover`,
58
+ strong: `${base}-strong`,
59
+ softBg: `${base}-soft-bg`,
60
+ softHover: `${base}-soft-hover`,
61
+ softActive: `${base}-soft-active`,
62
+ outline: `${base}-outline`,
63
+ onSolidText: `${base}-on-solid-text`,
64
+ };
65
+ }
66
+
67
+ function createTestTheme(): AnkhTheme {
68
+ return {
69
+ colors: {
70
+ primary: '#0f766e',
71
+ secondary: '#2563eb',
72
+ accent: '#7c3aed',
73
+ highlight: '#f59e0b',
74
+ background: '#ffffff',
75
+ surface: '#ffffff',
76
+ text: '#111827',
77
+ textSecondary: '#4b5563',
78
+ border: '#d1d5db',
79
+ error: '#dc2626',
80
+ success: '#16a34a',
81
+ warning: '#d97706',
82
+ },
83
+ config: {
84
+ id: 'zora-test',
85
+ name: 'ZORA Test',
86
+ light: {
87
+ primaryColor: '#0f766e',
88
+ harmony: 'analogous',
89
+ systemTone: 'jewel',
90
+ },
91
+ dark: {
92
+ primaryColor: '#2dd4bf',
93
+ harmony: 'analogous',
94
+ systemTone: 'jewel',
95
+ },
96
+ },
97
+ radii: {
98
+ none: 0,
99
+ s: 4,
100
+ m: 8,
101
+ l: 16,
102
+ full: 9999,
103
+ },
104
+ scales: {},
105
+ semantics: {
106
+ neutral: {
107
+ bg: '#ffffff',
108
+ bgSubtle: '#f9fafb',
109
+ surface: '#ffffff',
110
+ surfaceHover: '#f3f4f6',
111
+ surfaceActive: '#e5e7eb',
112
+ border: '#d1d5db',
113
+ borderStrong: '#9ca3af',
114
+ divider: '#e5e7eb',
115
+ text: '#111827',
116
+ textMuted: '#4b5563',
117
+ textSubtle: '#6b7280',
118
+ },
119
+ brand: createRole('#0f766e'),
120
+ secondary: createRole('#2563eb'),
121
+ accent: createRole('#7c3aed'),
122
+ highlight: createRole('#f59e0b'),
123
+ danger: createRole('#dc2626'),
124
+ success: createRole('#16a34a'),
125
+ warning: createRole('#d97706'),
126
+ surface: {
127
+ default: '#ffffff',
128
+ subtle: '#f9fafb',
129
+ raised: '#ffffff',
130
+ },
131
+ content: {
132
+ default: '#111827',
133
+ muted: '#4b5563',
134
+ subtle: '#6b7280',
135
+ inverse: '#ffffff',
136
+ },
137
+ border: {
138
+ default: '#d1d5db',
139
+ strong: '#9ca3af',
140
+ focus: '#0f766e',
141
+ },
142
+ action: {
143
+ primary: createRole('#0f766e'),
144
+ neutral: createRole('#4b5563'),
145
+ danger: createRole('#dc2626'),
146
+ },
147
+ },
148
+ shadows: {
149
+ soft: 2,
150
+ medium: 4,
151
+ hard: 8,
152
+ },
153
+ spacing: {
154
+ none: 0,
155
+ xs: 4,
156
+ s: 8,
157
+ m: 16,
158
+ l: 24,
159
+ xl: 32,
160
+ xxl: 48,
161
+ },
162
+ typography: {
163
+ headings: {
164
+ 1: { size: 32, lineHeight: 40, weight: 'bold' },
165
+ 2: { size: 24, lineHeight: 32, weight: 'bold' },
166
+ 3: { size: 20, lineHeight: 28, weight: 'bold' },
167
+ 4: { size: 18, lineHeight: 24, weight: 'semiBold' },
168
+ 5: { size: 16, lineHeight: 22, weight: 'semiBold' },
169
+ 6: { size: 14, lineHeight: 20, weight: 'semiBold' },
170
+ },
171
+ sizes: {
172
+ xs: 12,
173
+ s: 14,
174
+ m: 16,
175
+ l: 18,
176
+ xl: 20,
177
+ xxl: 24,
178
+ '3xl': 30,
179
+ h1: 32,
180
+ h2: 24,
181
+ h3: 20,
182
+ h4: 18,
183
+ h5: 16,
184
+ h6: 14,
185
+ },
186
+ weights: {
187
+ thin: '100',
188
+ extraLight: '200',
189
+ light: '300',
190
+ regular: '400',
191
+ medium: '500',
192
+ semiBold: '600',
193
+ bold: '700',
194
+ extraBold: '800',
195
+ black: '900',
196
+ },
197
+ fonts: {
198
+ normal: { ...emptyFonts },
199
+ italic: { ...emptyFonts },
200
+ },
201
+ },
202
+ };
203
+ }
204
+
205
+ const theme = createTestTheme();
206
+
207
+ describe('resolveTextStyle', () => {
208
+ test('resolves default body text from theme typography and content color', () => {
209
+ const style = resolveTextStyle({ theme, breakpoint: 'base' });
210
+
211
+ expect(style.fontSize).toBe(theme.typography.sizes.m);
212
+ expect(style.lineHeight).toBe(24);
213
+ expect(style.fontWeight).toBe(theme.typography.weights.regular);
214
+ expect(style.color).toBe(theme.semantics.content.default);
215
+ expect(style.elevation).toBe(0);
216
+ });
217
+
218
+ test('maps bodySmall, caption, and label recipes', () => {
219
+ expect(resolveTextStyle({ theme, breakpoint: 'base', variant: 'bodySmall' })).toMatchObject({
220
+ fontSize: theme.typography.sizes.s,
221
+ lineHeight: 20,
222
+ fontWeight: theme.typography.weights.regular,
223
+ });
224
+
225
+ expect(resolveTextStyle({ theme, breakpoint: 'base', variant: 'caption' })).toMatchObject({
226
+ fontSize: theme.typography.sizes.xs,
227
+ lineHeight: 16,
228
+ fontWeight: theme.typography.weights.regular,
229
+ });
230
+
231
+ expect(resolveTextStyle({ theme, breakpoint: 'base', variant: 'label' })).toMatchObject({
232
+ fontSize: theme.typography.sizes.s,
233
+ lineHeight: 18,
234
+ fontWeight: theme.typography.weights.medium,
235
+ });
236
+ });
237
+
238
+ test('resolves lead smaller on base and larger on md', () => {
239
+ expect(resolveTextStyle({ theme, breakpoint: 'base', variant: 'lead' })).toMatchObject({
240
+ fontSize: theme.typography.sizes.m,
241
+ lineHeight: 24,
242
+ });
243
+
244
+ expect(resolveTextStyle({ theme, breakpoint: 'md', variant: 'lead' })).toMatchObject({
245
+ fontSize: theme.typography.sizes.l,
246
+ lineHeight: 28,
247
+ });
248
+ });
249
+
250
+ test('resolves eyebrow uppercase, letter spacing, and semibold weight', () => {
251
+ const style = resolveTextStyle({ theme, breakpoint: 'base', variant: 'eyebrow' });
252
+
253
+ expect(style.textTransform).toBe('uppercase');
254
+ expect(style.letterSpacing).toBe(0.8);
255
+ expect(style.fontWeight).toBe(theme.typography.weights.semiBold);
256
+ });
257
+
258
+ test('resolves code as monospace text', () => {
259
+ expect(resolveTextStyle({ theme, breakpoint: 'base', variant: 'code' }).fontFamily).toBe(
260
+ 'monospace',
261
+ );
262
+ });
263
+
264
+ test('maps tones to semantic colors', () => {
265
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'default' }).color).toBe(
266
+ theme.semantics.content.default,
267
+ );
268
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'muted' }).color).toBe(
269
+ theme.semantics.content.muted,
270
+ );
271
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'subtle' }).color).toBe(
272
+ theme.semantics.content.subtle,
273
+ );
274
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'inverse' }).color).toBe(
275
+ theme.semantics.content.inverse,
276
+ );
277
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'primary' }).color).toBe(
278
+ theme.semantics.brand.base,
279
+ );
280
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'danger' }).color).toBe(
281
+ theme.semantics.danger.base,
282
+ );
283
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'success' }).color).toBe(
284
+ theme.semantics.success.base,
285
+ );
286
+ expect(resolveTextStyle({ theme, breakpoint: 'base', tone: 'warning' }).color).toBe(
287
+ theme.semantics.warning.base,
288
+ );
289
+ });
290
+
291
+ test('allows explicit weight to override the recipe weight', () => {
292
+ const style = resolveTextStyle({
293
+ theme,
294
+ breakpoint: 'base',
295
+ variant: 'caption',
296
+ weight: 'bold',
297
+ });
298
+
299
+ expect(style.fontWeight).toBe(theme.typography.weights.bold);
300
+ });
301
+
302
+ test('uses italic font family when available', () => {
303
+ const fontTheme = createTestTheme();
304
+ fontTheme.typography.fonts.italic[fontTheme.typography.weights.medium] = 'ZoraMediumItalic';
305
+
306
+ const style = resolveTextStyle({
307
+ theme: fontTheme,
308
+ breakpoint: 'base',
309
+ italic: true,
310
+ weight: 'medium',
311
+ });
312
+
313
+ expect(style.fontStyle).toBe('italic');
314
+ expect(style.fontFamily).toBe('ZoraMediumItalic');
315
+ });
316
+
317
+ test('resolves responsive variant, tone, align, and weight values', () => {
318
+ const style = resolveTextStyle({
319
+ theme,
320
+ breakpoint: 'md',
321
+ variant: { base: 'bodySmall', md: 'lead' },
322
+ tone: { base: 'muted', md: 'primary' },
323
+ align: { base: 'center', md: 'left' },
324
+ weight: { base: 'regular', md: 'semiBold' },
325
+ });
326
+
327
+ expect(style.fontSize).toBe(theme.typography.sizes.l);
328
+ expect(style.lineHeight).toBe(28);
329
+ expect(style.color).toBe(theme.semantics.brand.base);
330
+ expect(style.textAlign).toBe('left');
331
+ expect(style.fontWeight).toBe(theme.typography.weights.semiBold);
332
+ });
333
+ });