@newtonedev/components 0.1.6 → 0.1.8
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.
- package/dist/composites/actions/Button/Button.d.ts.map +1 -1
- package/dist/composites/form-controls/Select/Select.styles.d.ts.map +1 -1
- package/dist/composites/form-controls/TextInput/TextInput.styles.d.ts.map +1 -1
- package/dist/composites/form-controls/Toggle/Toggle.styles.d.ts.map +1 -1
- package/dist/composites/range-inputs/ColorScaleSlider/ColorScaleSlider.styles.d.ts.map +1 -1
- package/dist/composites/range-inputs/HueSlider/HueSlider.styles.d.ts.map +1 -1
- package/dist/composites/range-inputs/Slider/Slider.styles.d.ts.map +1 -1
- package/dist/fonts/GoogleFontLoader.d.ts +5 -4
- package/dist/fonts/GoogleFontLoader.d.ts.map +1 -1
- package/dist/fonts/SelfHostedFontLoader.d.ts +14 -0
- package/dist/fonts/SelfHostedFontLoader.d.ts.map +1 -0
- package/dist/fonts/buildGoogleFontsUrl.d.ts +1 -16
- package/dist/fonts/buildGoogleFontsUrl.d.ts.map +1 -1
- package/dist/fonts/measureFont.d.ts +18 -0
- package/dist/fonts/measureFont.d.ts.map +1 -0
- package/dist/fonts/reportQueue.d.ts +7 -0
- package/dist/fonts/reportQueue.d.ts.map +1 -0
- package/dist/fonts/useLocalCalibration.d.ts +19 -0
- package/dist/fonts/useLocalCalibration.d.ts.map +1 -0
- package/dist/fonts/useTypographyCalibrations.d.ts +11 -0
- package/dist/fonts/useTypographyCalibrations.d.ts.map +1 -0
- package/dist/index.cjs +863 -437
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +801 -391
- package/dist/index.js.map +1 -1
- package/dist/primitives/Icon/Icon.d.ts +1 -1
- package/dist/primitives/Icon/Icon.d.ts.map +1 -1
- package/dist/primitives/Icon/Icon.types.d.ts +8 -13
- package/dist/primitives/Icon/Icon.types.d.ts.map +1 -1
- package/dist/primitives/Text/Text.d.ts +33 -8
- package/dist/primitives/Text/Text.d.ts.map +1 -1
- package/dist/primitives/Text/Text.spans.d.ts +22 -0
- package/dist/primitives/Text/Text.spans.d.ts.map +1 -0
- package/dist/primitives/Text/Text.types.d.ts +75 -27
- package/dist/primitives/Text/Text.types.d.ts.map +1 -1
- package/dist/primitives/Text/index.d.ts +23 -2
- package/dist/primitives/Text/index.d.ts.map +1 -1
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/registry/codegen.d.ts.map +1 -1
- package/dist/registry/icons.d.ts +7 -0
- package/dist/registry/icons.d.ts.map +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/types.d.ts +3 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/theme/NewtoneProvider.d.ts +9 -1
- package/dist/theme/NewtoneProvider.d.ts.map +1 -1
- package/dist/theme/defaults.d.ts +1 -0
- package/dist/theme/defaults.d.ts.map +1 -1
- package/dist/theme/types.d.ts +48 -32
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/useBreakpoint.d.ts +9 -0
- package/dist/theme/useBreakpoint.d.ts.map +1 -0
- package/dist/tokens/computeTokens.d.ts +9 -22
- package/dist/tokens/computeTokens.d.ts.map +1 -1
- package/dist/tokens/types.d.ts +40 -22
- package/dist/tokens/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/composites/actions/Button/Button.styles.ts +3 -3
- package/src/composites/actions/Button/Button.tsx +3 -2
- package/src/composites/form-controls/Select/Select.styles.ts +8 -8
- package/src/composites/form-controls/Select/Select.tsx +1 -1
- package/src/composites/form-controls/Select/SelectOption.tsx +3 -3
- package/src/composites/form-controls/TextInput/TextInput.styles.ts +5 -5
- package/src/composites/form-controls/Toggle/Toggle.styles.ts +3 -3
- package/src/composites/range-inputs/ColorScaleSlider/ColorScaleSlider.styles.ts +6 -6
- package/src/composites/range-inputs/HueSlider/HueSlider.styles.ts +9 -9
- package/src/composites/range-inputs/Slider/Slider.styles.ts +9 -9
- package/src/fonts/GoogleFontLoader.tsx +25 -10
- package/src/fonts/SelfHostedFontLoader.tsx +44 -0
- package/src/fonts/buildGoogleFontsUrl.ts +2 -31
- package/src/fonts/measureFont.ts +42 -0
- package/src/fonts/reportQueue.ts +54 -0
- package/src/fonts/useLocalCalibration.ts +97 -0
- package/src/fonts/useTypographyCalibrations.ts +15 -0
- package/src/index.ts +18 -7
- package/src/primitives/Frame/Frame.tsx +3 -3
- package/src/primitives/Icon/Icon.tsx +5 -7
- package/src/primitives/Icon/Icon.types.ts +8 -15
- package/src/primitives/Text/Text.spans.ts +57 -0
- package/src/primitives/Text/Text.tsx +205 -53
- package/src/primitives/Text/Text.types.ts +80 -27
- package/src/primitives/Text/index.ts +27 -3
- package/src/primitives/index.ts +3 -2
- package/src/registry/codegen.ts +1 -0
- package/src/registry/icons.ts +111 -0
- package/src/registry/index.ts +3 -0
- package/src/registry/registry.ts +67 -70
- package/src/registry/types.ts +3 -1
- package/src/theme/NewtoneProvider.tsx +18 -2
- package/src/theme/defaults.ts +8 -28
- package/src/theme/types.ts +63 -33
- package/src/theme/useBreakpoint.ts +14 -0
- package/src/tokens/computeTokens.ts +23 -19
- package/src/tokens/types.ts +10 -24
- package/dist/fonts/googleFonts.d.ts +0 -20
- package/dist/fonts/googleFonts.d.ts.map +0 -1
- package/src/fonts/googleFonts.ts +0 -87
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import { Text as RNText } from 'react-native';
|
|
3
|
-
import type { TextStyle } from 'react-native';
|
|
1
|
+
import React, { createContext, useMemo, useState, useEffect } from 'react';
|
|
2
|
+
import { Text as RNText, View } from 'react-native';
|
|
3
|
+
import type { LayoutChangeEvent, TextStyle } from 'react-native';
|
|
4
4
|
import { srgbToHex } from 'newtone';
|
|
5
|
+
import { resolveResponsiveSize, estimateLineWidths, BREAKPOINT_ROLE_SCALE, scaleRoleStep, REFERENCE_LINE_HEIGHT_RATIO, buildFontFeatureSettings, SEMANTIC_WEIGHT_MAP, ROLE_DEFAULT_WEIGHTS } from '@newtonedev/fonts';
|
|
5
6
|
import { useTokens } from '../../tokens/useTokens';
|
|
7
|
+
import { useNewtoneTheme } from '../../theme/NewtoneProvider';
|
|
6
8
|
import type { UseTokensResult } from '../../tokens/useTokens';
|
|
7
9
|
import type { TextProps, TextColor } from './Text.types';
|
|
10
|
+
import { useLocalCalibration } from '../../fonts/useLocalCalibration';
|
|
11
|
+
import { useTypographyCalibrations } from '../../fonts/useTypographyCalibrations';
|
|
12
|
+
import { enqueueObservation } from '../../fonts/reportQueue';
|
|
13
|
+
import { useBreakpoint } from '../../theme/useBreakpoint';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Context to pass the active scope's weight map to span sub-components.
|
|
17
|
+
* Spans inherit font family from the parent RN Text, but need the scope's
|
|
18
|
+
* weight mapping to resolve semantic weight tokens (regular/medium/bold)
|
|
19
|
+
* to numeric CSS values.
|
|
20
|
+
*/
|
|
21
|
+
export const TextScopeContext = createContext<{
|
|
22
|
+
readonly weights: { readonly regular: number; readonly medium: number; readonly bold: number };
|
|
23
|
+
} | null>(null);
|
|
8
24
|
|
|
9
25
|
/**
|
|
10
26
|
* Resolve a semantic text color to a hex string from the current tokens.
|
|
11
27
|
* Neutral text colors are top-level; palette colors use nested PaletteTokens.fill.
|
|
12
28
|
*/
|
|
13
|
-
function resolveTextColor(color: TextColor, tokens: UseTokensResult): string {
|
|
29
|
+
export function resolveTextColor(color: TextColor, tokens: UseTokensResult): string {
|
|
14
30
|
switch (color) {
|
|
15
31
|
case 'primary': return srgbToHex(tokens.textPrimary.srgb);
|
|
16
32
|
case 'secondary': return srgbToHex(tokens.textSecondary.srgb);
|
|
@@ -23,78 +39,214 @@ function resolveTextColor(color: TextColor, tokens: UseTokensResult): string {
|
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
41
|
|
|
42
|
+
/** Adaptive roles that support responsive font sizing. */
|
|
43
|
+
const ADAPTIVE_ROLES = new Set(['headline', 'title', 'heading', 'subheading']);
|
|
44
|
+
|
|
45
|
+
/** Maps heading-like roles to HTML heading levels for semantic rendering on web. */
|
|
46
|
+
const ROLE_HEADING_LEVEL: Readonly<Record<string, number>> = {
|
|
47
|
+
headline: 1,
|
|
48
|
+
title: 2,
|
|
49
|
+
heading: 3,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Estimate total character count from potentially nested React children. */
|
|
53
|
+
function extractCharacterCount(node: React.ReactNode): number {
|
|
54
|
+
if (typeof node === 'string') return node.length;
|
|
55
|
+
if (typeof node === 'number') return String(node).length;
|
|
56
|
+
if (!node) return 0;
|
|
57
|
+
if (Array.isArray(node)) {
|
|
58
|
+
return node.reduce((sum: number, child) => sum + extractCharacterCount(child), 0);
|
|
59
|
+
}
|
|
60
|
+
if (typeof node === 'object' && 'props' in node) {
|
|
61
|
+
return extractCharacterCount((node as React.ReactElement).props?.children);
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
/**
|
|
27
|
-
* Token-aware text primitive.
|
|
67
|
+
* Token-aware text primitive with semantic scope + role API.
|
|
28
68
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
69
|
+
* - `scope` selects the font family (main, display, mono, currency)
|
|
70
|
+
* - `role` sets default weight; fontSize and lineHeight resolved from config's role mapping
|
|
71
|
+
* - `size` selects within the role's 3-step scale (sm, md, lg)
|
|
72
|
+
* - `responsive` enables container-width-aware font sizing for adaptive roles
|
|
31
73
|
*
|
|
32
74
|
* @example
|
|
33
75
|
* ```tsx
|
|
34
|
-
* <Text>Body text</Text>
|
|
35
|
-
* <Text
|
|
36
|
-
* <Text
|
|
76
|
+
* <Text>Body text (default)</Text>
|
|
77
|
+
* <Text role="headline" scope="display">Hero title</Text>
|
|
78
|
+
* <Text role="label">Form label</Text>
|
|
79
|
+
* <Text scope="mono" role="caption">const x = 42;</Text>
|
|
80
|
+
* <Text role="body">Regular <Text.Bold>bold</Text.Bold> text</Text>
|
|
81
|
+
* <Text role="headline" responsive>Responsive headline</Text>
|
|
37
82
|
* ```
|
|
38
83
|
*/
|
|
39
|
-
|
|
84
|
+
function TextBase({
|
|
40
85
|
children,
|
|
41
|
-
|
|
42
|
-
|
|
86
|
+
scope = 'main',
|
|
87
|
+
role = 'body',
|
|
43
88
|
color = 'primary',
|
|
44
|
-
|
|
45
|
-
lineHeight = 'normal',
|
|
89
|
+
size: sizeOverride,
|
|
46
90
|
align,
|
|
47
91
|
numberOfLines,
|
|
48
92
|
elevation = 1,
|
|
49
93
|
style,
|
|
50
|
-
|
|
51
|
-
accessibilityRole,
|
|
52
|
-
// Testing & platform
|
|
94
|
+
accessibilityRole: accessibilityRoleOverride,
|
|
53
95
|
testID,
|
|
54
96
|
nativeID,
|
|
55
97
|
ref,
|
|
98
|
+
responsive = false,
|
|
99
|
+
centerVertically = false,
|
|
100
|
+
features,
|
|
56
101
|
}: TextProps) {
|
|
57
|
-
// Get the current theme's design tokens (colors, fonts, spacing).
|
|
58
|
-
// The elevation parameter affects which background shade the tokens reference.
|
|
59
102
|
const tokens = useTokens(elevation);
|
|
103
|
+
const { config, reportingEndpoint } = useNewtoneTheme();
|
|
104
|
+
|
|
105
|
+
const size = sizeOverride ?? 'md';
|
|
106
|
+
const fontSlot = tokens.typography.fonts[scope];
|
|
107
|
+
// roleWeights from config (user-customized), falling back to canonical defaults
|
|
108
|
+
const resolvedFontWeight = config.typography.roleWeights?.[role] ?? ROLE_DEFAULT_WEIGHTS[role];
|
|
109
|
+
|
|
110
|
+
// --- Breakpoint-aware step ---
|
|
111
|
+
const breakpoint = useBreakpoint();
|
|
112
|
+
const baseStep = config.typography.roles[role][size];
|
|
113
|
+
const bpScale = BREAKPOINT_ROLE_SCALE[breakpoint][role];
|
|
114
|
+
const step = bpScale === 1.0 ? baseStep : scaleRoleStep(baseStep, bpScale);
|
|
115
|
+
|
|
116
|
+
// --- Responsive sizing (Layer 2 calibration) ---
|
|
117
|
+
// Hooks are called unconditionally; results are used only when `responsive && isAdaptive`.
|
|
118
|
+
const calibrations = useTypographyCalibrations();
|
|
119
|
+
const fontSlotFull = config.typography.fonts[scope]; // FontSlot with config.fallback
|
|
120
|
+
const localRatio = useLocalCalibration(
|
|
121
|
+
fontSlot.family,
|
|
122
|
+
SEMANTIC_WEIGHT_MAP.regular,
|
|
123
|
+
fontSlotFull.config.fallback,
|
|
124
|
+
calibrations[fontSlot.family],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const isAdaptive = ADAPTIVE_ROLES.has(role);
|
|
128
|
+
const [containerWidth, setContainerWidth] = useState<number | null>(null);
|
|
129
|
+
const characterCount = useMemo(() => extractCharacterCount(children), [children]);
|
|
130
|
+
|
|
131
|
+
const resolvedStep = useMemo(
|
|
132
|
+
() =>
|
|
133
|
+
resolveResponsiveSize(
|
|
134
|
+
{
|
|
135
|
+
role,
|
|
136
|
+
size,
|
|
137
|
+
responsive: responsive && isAdaptive,
|
|
138
|
+
fontFamily: fontSlot.family,
|
|
139
|
+
maxFontSize: step.fontSize,
|
|
140
|
+
minFontSize: Math.max(8, Math.round(step.fontSize * 0.7)),
|
|
141
|
+
},
|
|
142
|
+
config.typography.roles,
|
|
143
|
+
containerWidth != null ? { containerWidth, characterCount } : undefined,
|
|
144
|
+
{ [fontSlot.family]: localRatio },
|
|
145
|
+
),
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
[role, size, responsive, isAdaptive, fontSlot.family, step.fontSize, config.typography.roles, containerWidth, characterCount, localRatio],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// --- Layer 3: report observations for cross-site rolling averages ---
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!reportingEndpoint || !responsive || !isAdaptive || containerWidth == null) return;
|
|
153
|
+
const lineWidths = estimateLineWidths(characterCount, containerWidth, resolvedStep.fontSize, localRatio);
|
|
154
|
+
const lastLine = lineWidths[lineWidths.length - 1];
|
|
155
|
+
enqueueObservation(reportingEndpoint, {
|
|
156
|
+
fontFamily: fontSlot.family,
|
|
157
|
+
fontWeight: resolvedFontWeight,
|
|
158
|
+
role,
|
|
159
|
+
size,
|
|
160
|
+
fontSize: resolvedStep.fontSize,
|
|
161
|
+
containerWidth,
|
|
162
|
+
characterCount,
|
|
163
|
+
lineCount: lineWidths.length,
|
|
164
|
+
lastLineRatio: containerWidth > 0 ? lastLine / containerWidth : 1,
|
|
165
|
+
});
|
|
166
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
|
+
}, [reportingEndpoint, resolvedStep.fontSize, containerWidth]);
|
|
60
168
|
|
|
61
|
-
// Build the text style from the theme tokens.
|
|
62
|
-
// Wrapped in useMemo so it only recalculates when the inputs actually change,
|
|
63
|
-
// instead of recalculating on every render (which would be wasteful).
|
|
64
169
|
const resolvedStyle = useMemo<TextStyle>(() => {
|
|
65
|
-
|
|
66
|
-
|
|
170
|
+
const activeStep = responsive && isAdaptive ? resolvedStep : step;
|
|
171
|
+
|
|
172
|
+
// Font metrics corrections (adjustLineHeight, verticalCenterOffset, features)
|
|
173
|
+
const currentMetrics = config.typography.fontMetrics?.[fontSlot.family];
|
|
174
|
+
|
|
175
|
+
// Phase 3: Correct line height for font's natural vertical extent (snap to 4px grid)
|
|
176
|
+
const correctedLineHeight = currentMetrics
|
|
177
|
+
? Math.round((activeStep.lineHeight * currentMetrics.naturalLineHeightRatio / REFERENCE_LINE_HEIGHT_RATIO) / 4) * 4
|
|
178
|
+
: activeStep.lineHeight;
|
|
179
|
+
|
|
180
|
+
// Phase 4: Visual centering correction (opt-in via centerVertically prop, snap to 0.5px)
|
|
181
|
+
const vcOffset = centerVertically && currentMetrics
|
|
182
|
+
? Math.round(currentMetrics.verticalCenterOffset * activeStep.fontSize * 2) / 2
|
|
183
|
+
: 0;
|
|
184
|
+
|
|
185
|
+
// Phase 5: OpenType feature settings
|
|
186
|
+
const activeFeatures = features
|
|
187
|
+
? (currentMetrics?.features ? features.filter(tag => currentMetrics.features.includes(tag)) : [...features])
|
|
188
|
+
: [];
|
|
189
|
+
const featureSettings = activeFeatures.length > 0 ? buildFontFeatureSettings(activeFeatures) : undefined;
|
|
190
|
+
|
|
67
191
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Font weight is stored as a number (e.g. 400, 600) but React Native expects a string.
|
|
72
|
-
fontWeight: String(tokens.typography.weight[weight]) as TextStyle['fontWeight'],
|
|
73
|
-
// Convert the theme color from internal sRGB format to a hex string (e.g. '#1a1a1a').
|
|
192
|
+
fontFamily: fontSlot.family,
|
|
193
|
+
fontSize: activeStep.fontSize,
|
|
194
|
+
fontWeight: String(resolvedFontWeight) as TextStyle['fontWeight'],
|
|
74
195
|
color: resolveTextColor(color, tokens),
|
|
75
|
-
|
|
76
|
-
lineHeight: fontSize * tokens.typography.lineHeight[lineHeight],
|
|
196
|
+
lineHeight: correctedLineHeight,
|
|
77
197
|
textAlign: align,
|
|
198
|
+
...(vcOffset !== 0 ? { transform: [{ translateY: vcOffset }] } : {}),
|
|
199
|
+
...(featureSettings ? { fontFeatureSettings: featureSettings } as any : {}),
|
|
78
200
|
};
|
|
79
|
-
}, [tokens,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
201
|
+
}, [tokens, fontSlot, step, resolvedStep, responsive, isAdaptive, resolvedFontWeight, color, align, config.typography.fontMetrics, centerVertically, features]);
|
|
202
|
+
|
|
203
|
+
// Auto-derive accessibility role and heading level for heading-like typography roles.
|
|
204
|
+
// react-native-web uses aria-level to render the correct <h1>/<h2>/<h3> element.
|
|
205
|
+
const inferredA11yRole = (role === 'headline' || role === 'title' || role === 'heading')
|
|
206
|
+
? 'header' as const
|
|
207
|
+
: undefined;
|
|
208
|
+
const effectiveA11yRole = accessibilityRoleOverride ?? inferredA11yRole;
|
|
209
|
+
const ariaLevel = effectiveA11yRole === 'header' ? ROLE_HEADING_LEVEL[role] : undefined;
|
|
210
|
+
|
|
211
|
+
// Scope context value for span sub-components (stable reference — weights are constant)
|
|
212
|
+
const scopeCtx = useMemo(() => ({ weights: SEMANTIC_WEIGHT_MAP }), []);
|
|
213
|
+
|
|
214
|
+
const textNode = (
|
|
215
|
+
<TextScopeContext.Provider value={scopeCtx}>
|
|
216
|
+
<RNText
|
|
217
|
+
ref={ref}
|
|
218
|
+
testID={testID}
|
|
219
|
+
nativeID={nativeID}
|
|
220
|
+
accessibilityRole={effectiveA11yRole}
|
|
221
|
+
aria-level={ariaLevel}
|
|
222
|
+
style={style
|
|
223
|
+
? [resolvedStyle, ...(Array.isArray(style) ? style : [style])]
|
|
224
|
+
: resolvedStyle
|
|
225
|
+
}
|
|
226
|
+
numberOfLines={numberOfLines}
|
|
227
|
+
>
|
|
228
|
+
{children}
|
|
229
|
+
</RNText>
|
|
230
|
+
</TextScopeContext.Provider>
|
|
99
231
|
);
|
|
232
|
+
|
|
233
|
+
// Wrap in a View for container measurement when responsive mode is active on an adaptive role.
|
|
234
|
+
// onLayout fires whenever the container's dimensions change, providing the measured width.
|
|
235
|
+
if (responsive && isAdaptive) {
|
|
236
|
+
return (
|
|
237
|
+
<View
|
|
238
|
+
onLayout={(e: LayoutChangeEvent) => {
|
|
239
|
+
const w = e.nativeEvent.layout.width;
|
|
240
|
+
if (w > 0) setContainerWidth(w);
|
|
241
|
+
}}
|
|
242
|
+
style={{ width: '100%' }}
|
|
243
|
+
>
|
|
244
|
+
{textNode}
|
|
245
|
+
</View>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return textNode;
|
|
100
250
|
}
|
|
251
|
+
|
|
252
|
+
export { TextBase };
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { Text as RNText, TextStyle } from 'react-native';
|
|
2
2
|
import type { ElevationLevel } from '../../theme/types';
|
|
3
3
|
|
|
4
|
-
/** Typography size
|
|
5
|
-
export type TextSize = '
|
|
4
|
+
/** Typography size step — selects within the current role's 3-step scale. @default 'md' */
|
|
5
|
+
export type TextSize = 'sm' | 'md' | 'lg';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
export type TextWeight = 'regular' | 'medium' | '
|
|
7
|
+
/** Font weight slots — maps to per-scope `tokens.typography.fonts[scope].weights.*` values. */
|
|
8
|
+
export type TextWeight = 'regular' | 'medium' | 'bold';
|
|
9
9
|
|
|
10
10
|
/** Semantic text color tokens — resolved from the current theme's token palette. */
|
|
11
11
|
export type TextColor = 'primary' | 'secondary' | 'tertiary' | 'disabled' | 'accent' | 'success' | 'warning' | 'error';
|
|
12
12
|
|
|
13
|
-
/** Font
|
|
14
|
-
export type
|
|
13
|
+
/** Font scope — selects which font family slot to use. @default 'main' */
|
|
14
|
+
export type TextScope = 'main' | 'display' | 'mono' | 'currency';
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Semantic text role — determines default size, weight, and line height.
|
|
18
|
+
* Provides purpose-driven typography presets. Individual size/weight/lineHeight
|
|
19
|
+
* props can override the role's defaults when needed (e.g., in composites).
|
|
20
|
+
* @default 'body'
|
|
21
|
+
*/
|
|
22
|
+
export type TextRole = 'headline' | 'title' | 'heading' | 'subheading' | 'body' | 'label' | 'caption';
|
|
18
23
|
|
|
19
24
|
/** Text alignment. */
|
|
20
25
|
export type TextAlign = 'left' | 'center' | 'right';
|
|
@@ -22,38 +27,37 @@ export type TextAlign = 'left' | 'center' | 'right';
|
|
|
22
27
|
/**
|
|
23
28
|
* Props for Text — themed typography component.
|
|
24
29
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* tokens rather than raw values for consistency.
|
|
30
|
+
* Primary API surface: `scope` (font family) + `role` (semantic preset) + `color`.
|
|
31
|
+
* Individual size/weight/lineHeight props are available as overrides for composites.
|
|
28
32
|
*
|
|
29
33
|
* @example
|
|
30
34
|
* ```tsx
|
|
31
35
|
* <Text>Default body text</Text>
|
|
32
|
-
* <Text
|
|
33
|
-
* <Text
|
|
34
|
-
* <Text
|
|
35
|
-
* <Text
|
|
36
|
+
* <Text role="headline" scope="display">Hero title</Text>
|
|
37
|
+
* <Text role="label">Form label</Text>
|
|
38
|
+
* <Text scope="mono" role="caption">const x = 42;</Text>
|
|
39
|
+
* <Text role="body">Regular text with <Text.Bold>bold emphasis</Text.Bold></Text>
|
|
36
40
|
* ```
|
|
37
41
|
*/
|
|
38
42
|
export interface TextProps {
|
|
39
43
|
/**
|
|
40
|
-
* Text content. Accepts strings, numbers, and
|
|
41
|
-
* for inline formatting (e.g.,
|
|
44
|
+
* Text content. Accepts strings, numbers, and span sub-components
|
|
45
|
+
* for inline formatting (e.g., `<Text.Bold>`, `<Text.Italic>`).
|
|
42
46
|
*
|
|
43
47
|
* @example
|
|
44
48
|
* ```tsx
|
|
45
49
|
* <Text>Plain string</Text>
|
|
46
50
|
* <Text>Count: {42}</Text>
|
|
47
|
-
* <Text>Normal <Text
|
|
51
|
+
* <Text role="body">Normal <Text.Bold>bold</Text.Bold> normal</Text>
|
|
48
52
|
* ```
|
|
49
53
|
*/
|
|
50
54
|
readonly children: React.ReactNode;
|
|
51
55
|
|
|
52
|
-
/**
|
|
53
|
-
readonly
|
|
56
|
+
/** Font scope — selects which font family to use. @default 'main' */
|
|
57
|
+
readonly scope?: TextScope;
|
|
54
58
|
|
|
55
|
-
/**
|
|
56
|
-
readonly
|
|
59
|
+
/** Semantic role — sets default size/weight/lineHeight. @default 'body' */
|
|
60
|
+
readonly role?: TextRole;
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
63
|
* Semantic color token.
|
|
@@ -71,11 +75,10 @@ export interface TextProps {
|
|
|
71
75
|
*/
|
|
72
76
|
readonly color?: TextColor;
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
readonly font?: TextFont;
|
|
78
|
+
// ── Overrides (used by composites, not exposed in editor UI) ──
|
|
76
79
|
|
|
77
|
-
/**
|
|
78
|
-
readonly
|
|
80
|
+
/** Override the role's default size. */
|
|
81
|
+
readonly size?: TextSize;
|
|
79
82
|
|
|
80
83
|
/** Text alignment. */
|
|
81
84
|
readonly align?: TextAlign;
|
|
@@ -92,7 +95,8 @@ export interface TextProps {
|
|
|
92
95
|
// ── Accessibility ──
|
|
93
96
|
|
|
94
97
|
/**
|
|
95
|
-
* Semantic role for screen readers.
|
|
98
|
+
* Semantic role for screen readers. Auto-inferred from typography role
|
|
99
|
+
* (headline/title/heading → 'header') unless explicitly set.
|
|
96
100
|
*
|
|
97
101
|
* - `'header'` — Marks this as a heading (e.g. page title, section header)
|
|
98
102
|
* - `'text'` — Default text role (usually omitted)
|
|
@@ -110,4 +114,53 @@ export interface TextProps {
|
|
|
110
114
|
|
|
111
115
|
/** Ref to the underlying React Native Text element. */
|
|
112
116
|
readonly ref?: React.Ref<RNText>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Enable responsive font sizing for adaptive roles (headline, title, heading, subheading).
|
|
120
|
+
* When true, the Text component measures its container width and uses `resolveResponsiveSize`
|
|
121
|
+
* to pick the best fontSize within the role's natural size, improving line-break quality.
|
|
122
|
+
* Requires the text to be wrapped in a container that defines a width.
|
|
123
|
+
* No-op for non-adaptive roles (body, label, caption).
|
|
124
|
+
* @default false
|
|
125
|
+
*/
|
|
126
|
+
readonly responsive?: boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Apply vertical centering correction to compensate for font metrics.
|
|
130
|
+
* Shifts text down by the font's verticalCenterOffset so that cap-height
|
|
131
|
+
* text appears visually centered within flex containers (buttons, chips).
|
|
132
|
+
* No-op for multi-line body text — only enable in single-line centering contexts.
|
|
133
|
+
* Requires fontMetrics in the theme config; ignored when metrics are unavailable.
|
|
134
|
+
* @default false
|
|
135
|
+
*/
|
|
136
|
+
readonly centerVertically?: boolean;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* OpenType feature tags to enable (e.g., `['tnum', 'kern']`).
|
|
140
|
+
* When fontMetrics are available, requested tags are filtered against the font's
|
|
141
|
+
* supported features. When metrics are unavailable, all tags are applied optimistically.
|
|
142
|
+
* Maps to the CSS `font-feature-settings` property via react-native-web.
|
|
143
|
+
*/
|
|
144
|
+
readonly features?: readonly string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Constrained props for inline span sub-components (Text.Bold, Text.Italic, etc.).
|
|
149
|
+
* Spans inherit font family, size, and line height from the parent Text.
|
|
150
|
+
* Only inline formatting properties are exposed.
|
|
151
|
+
*/
|
|
152
|
+
export interface TextSpanProps {
|
|
153
|
+
readonly children: React.ReactNode;
|
|
154
|
+
/** Override text color within the span. */
|
|
155
|
+
readonly color?: TextColor;
|
|
156
|
+
/** Override font weight within the span. */
|
|
157
|
+
readonly weight?: TextWeight;
|
|
158
|
+
/** Apply italic style. */
|
|
159
|
+
readonly italic?: boolean;
|
|
160
|
+
/** Apply underline decoration. */
|
|
161
|
+
readonly underline?: boolean;
|
|
162
|
+
/** Apply background highlight using a semantic color token. */
|
|
163
|
+
readonly highlight?: TextColor;
|
|
164
|
+
/** Style overrides (applied last). */
|
|
165
|
+
readonly style?: TextStyle | readonly TextStyle[];
|
|
113
166
|
}
|
|
@@ -1,10 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import { TextBase } from './Text';
|
|
2
|
+
import { TextSpan, TextBold, TextMedium, TextItalic, TextUnderline, TextHighlight } from './Text.spans';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Token-aware text primitive with span sub-components.
|
|
6
|
+
*
|
|
7
|
+
* Primary API: `scope` (font family) + `role` (semantic preset) + `color`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Text role="headline" scope="display">Hero</Text>
|
|
12
|
+
* <Text role="body">Regular <Text.Bold>bold</Text.Bold> text</Text>
|
|
13
|
+
* <Text role="label" color="secondary">Field label</Text>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const Text = Object.assign(TextBase, {
|
|
17
|
+
Span: TextSpan,
|
|
18
|
+
Bold: TextBold,
|
|
19
|
+
Medium: TextMedium,
|
|
20
|
+
Italic: TextItalic,
|
|
21
|
+
Underline: TextUnderline,
|
|
22
|
+
Highlight: TextHighlight,
|
|
23
|
+
});
|
|
24
|
+
|
|
2
25
|
export type {
|
|
3
26
|
TextProps,
|
|
4
27
|
TextSize,
|
|
5
28
|
TextWeight,
|
|
6
29
|
TextColor,
|
|
7
|
-
|
|
8
|
-
|
|
30
|
+
TextScope,
|
|
31
|
+
TextRole,
|
|
9
32
|
TextAlign,
|
|
33
|
+
TextSpanProps,
|
|
10
34
|
} from './Text.types';
|
package/src/primitives/index.ts
CHANGED
package/src/registry/codegen.ts
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface IconCatalogCategory {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly icons: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const ICON_CATALOG: readonly IconCatalogCategory[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'navigation',
|
|
10
|
+
label: 'Navigation',
|
|
11
|
+
icons: [
|
|
12
|
+
'home', 'menu', 'close', 'arrow_back', 'arrow_forward',
|
|
13
|
+
'arrow_upward', 'arrow_downward', 'chevron_left', 'chevron_right',
|
|
14
|
+
'expand_more', 'expand_less', 'first_page', 'last_page',
|
|
15
|
+
'more_vert', 'more_horiz', 'unfold_more', 'unfold_less',
|
|
16
|
+
'subdirectory_arrow_right',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'actions',
|
|
21
|
+
label: 'Actions',
|
|
22
|
+
icons: [
|
|
23
|
+
'search', 'settings', 'done', 'add', 'remove',
|
|
24
|
+
'delete', 'edit', 'save', 'refresh', 'undo',
|
|
25
|
+
'redo', 'download', 'upload', 'share', 'print',
|
|
26
|
+
'content_copy', 'content_paste', 'open_in_new', 'launch',
|
|
27
|
+
'drag_indicator', 'tune', 'sort', 'filter_list',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'communication',
|
|
32
|
+
label: 'Communication',
|
|
33
|
+
icons: [
|
|
34
|
+
'mail', 'chat', 'call', 'notifications', 'forum',
|
|
35
|
+
'send', 'inbox', 'drafts', 'mark_email_read',
|
|
36
|
+
'contact_mail', 'alternate_email', 'comment',
|
|
37
|
+
'chat_bubble', 'sms', 'voicemail',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'content',
|
|
42
|
+
label: 'Content',
|
|
43
|
+
icons: [
|
|
44
|
+
'add_circle', 'remove_circle', 'check_circle', 'cancel',
|
|
45
|
+
'flag', 'bookmark', 'star', 'favorite',
|
|
46
|
+
'thumb_up', 'thumb_down', 'push_pin', 'link',
|
|
47
|
+
'link_off', 'bolt', 'label', 'inventory_2',
|
|
48
|
+
'visibility', 'visibility_off',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'media',
|
|
53
|
+
label: 'Media',
|
|
54
|
+
icons: [
|
|
55
|
+
'play_arrow', 'pause', 'stop', 'skip_next', 'skip_previous',
|
|
56
|
+
'fast_forward', 'fast_rewind', 'replay', 'shuffle',
|
|
57
|
+
'repeat', 'volume_up', 'volume_off', 'music_note',
|
|
58
|
+
'image', 'photo_camera', 'videocam', 'mic',
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'file',
|
|
63
|
+
label: 'File & Folder',
|
|
64
|
+
icons: [
|
|
65
|
+
'folder', 'folder_open', 'create_new_folder', 'description',
|
|
66
|
+
'file_copy', 'attach_file', 'cloud', 'cloud_upload',
|
|
67
|
+
'cloud_download', 'cloud_off', 'storage', 'snippet_folder',
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'social',
|
|
72
|
+
label: 'Social & People',
|
|
73
|
+
icons: [
|
|
74
|
+
'person', 'group', 'person_add', 'person_remove',
|
|
75
|
+
'people', 'face', 'sentiment_satisfied', 'sentiment_dissatisfied',
|
|
76
|
+
'public', 'workspace_premium', 'emoji_events', 'military_tech',
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'alerts',
|
|
81
|
+
label: 'Alerts & Status',
|
|
82
|
+
icons: [
|
|
83
|
+
'error', 'warning', 'info', 'help',
|
|
84
|
+
'check_circle', 'report', 'new_releases',
|
|
85
|
+
'notification_important', 'priority_high',
|
|
86
|
+
'verified', 'shield', 'security',
|
|
87
|
+
'lock', 'lock_open',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'device',
|
|
92
|
+
label: 'Device & Hardware',
|
|
93
|
+
icons: [
|
|
94
|
+
'phone_android', 'computer', 'tablet_mac', 'watch',
|
|
95
|
+
'keyboard', 'mouse', 'headphones', 'speaker',
|
|
96
|
+
'monitor', 'devices', 'memory', 'battery_full',
|
|
97
|
+
'wifi', 'bluetooth', 'usb', 'dark_mode', 'light_mode',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'editor',
|
|
102
|
+
label: 'Editor & Formatting',
|
|
103
|
+
icons: [
|
|
104
|
+
'format_bold', 'format_italic', 'format_underlined',
|
|
105
|
+
'format_list_bulleted', 'format_list_numbered', 'format_quote',
|
|
106
|
+
'format_align_left', 'format_align_center', 'format_align_right',
|
|
107
|
+
'title', 'text_fields', 'code', 'palette',
|
|
108
|
+
'color_lens', 'brush', 'auto_fix_high',
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
];
|