@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference string for character width measurement.
|
|
3
|
+
* Covers uppercase, lowercase, digits, and a space — 63 characters total.
|
|
4
|
+
*/
|
|
5
|
+
const REF_STRING =
|
|
6
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Measure the average character width ratio for a font using the Canvas API.
|
|
10
|
+
*
|
|
11
|
+
* Waits for the font to load via `document.fonts.load()` before measuring.
|
|
12
|
+
* Falls back to 0.55 if the browser context is unavailable, the font fails
|
|
13
|
+
* to load, or canvas is not supported.
|
|
14
|
+
*
|
|
15
|
+
* The returned ratio represents `avgCharWidth / fontSize`. Multiply by a
|
|
16
|
+
* desired fontSize to estimate character widths for that font.
|
|
17
|
+
*
|
|
18
|
+
* @param fontFamily - Font family name, e.g. "Inter" or "system-ui"
|
|
19
|
+
* @param fontWeight - CSS font-weight number, e.g. 400
|
|
20
|
+
* @param fallback - CSS fallback stack, e.g. "sans-serif"
|
|
21
|
+
* @param fontSize - Reference font size in px (default 16)
|
|
22
|
+
* @returns Average character width ratio, e.g. 0.52 for Inter
|
|
23
|
+
*/
|
|
24
|
+
export async function measureAvgCharWidth(
|
|
25
|
+
fontFamily: string,
|
|
26
|
+
fontWeight: number,
|
|
27
|
+
fallback: string,
|
|
28
|
+
fontSize = 16,
|
|
29
|
+
): Promise<number> {
|
|
30
|
+
if (typeof document === 'undefined') return 0.55;
|
|
31
|
+
try {
|
|
32
|
+
await document.fonts.load(`${fontWeight} ${fontSize}px "${fontFamily}"`);
|
|
33
|
+
const canvas = document.createElement('canvas');
|
|
34
|
+
const ctx = canvas.getContext('2d');
|
|
35
|
+
if (!ctx) return 0.55;
|
|
36
|
+
ctx.font = `${fontWeight} ${fontSize}px "${fontFamily}", ${fallback}`;
|
|
37
|
+
const ratio = ctx.measureText(REF_STRING).width / REF_STRING.length / fontSize;
|
|
38
|
+
return Math.round(ratio * 1000) / 1000;
|
|
39
|
+
} catch {
|
|
40
|
+
return 0.55;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ObserverPayload } from '@newtonedev/fonts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module-level batch queue for typography observations (Layer 3 reporting).
|
|
5
|
+
*
|
|
6
|
+
* Lives outside React's component lifecycle so it survives re-renders.
|
|
7
|
+
* Observations are enqueued immediately and flushed in a single batch after
|
|
8
|
+
* a 2s debounce — minimising network requests and allowing coalescence.
|
|
9
|
+
*
|
|
10
|
+
* Usage: call `enqueueObservation(endpoint, payload)` from responsive Text
|
|
11
|
+
* instances. Set `reportingEndpoint` on `NewtoneProvider` to opt in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface QueuedObservation {
|
|
15
|
+
readonly endpoint: string;
|
|
16
|
+
readonly payload: ObserverPayload;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const queue: QueuedObservation[] = [];
|
|
20
|
+
let flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
21
|
+
|
|
22
|
+
function flush(): void {
|
|
23
|
+
if (queue.length === 0) return;
|
|
24
|
+
|
|
25
|
+
// Group by endpoint so a page with multiple endpoints gets one request each
|
|
26
|
+
const byEndpoint = new Map<string, ObserverPayload[]>();
|
|
27
|
+
for (const item of queue) {
|
|
28
|
+
const group = byEndpoint.get(item.endpoint) ?? [];
|
|
29
|
+
group.push(item.payload);
|
|
30
|
+
byEndpoint.set(item.endpoint, group);
|
|
31
|
+
}
|
|
32
|
+
queue.length = 0;
|
|
33
|
+
|
|
34
|
+
for (const [endpoint, observations] of byEndpoint) {
|
|
35
|
+
// Fire-and-forget — reporting failures are silent and never affect the UI
|
|
36
|
+
fetch(endpoint, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ observations }),
|
|
40
|
+
// keepalive: true allows the request to outlive the page
|
|
41
|
+
keepalive: true,
|
|
42
|
+
}).catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Enqueue a typography observation for batch reporting.
|
|
48
|
+
* Resets the 2s debounce timer on each call.
|
|
49
|
+
*/
|
|
50
|
+
export function enqueueObservation(endpoint: string, payload: ObserverPayload): void {
|
|
51
|
+
queue.push({ endpoint, payload });
|
|
52
|
+
if (flushTimer !== undefined) clearTimeout(flushTimer);
|
|
53
|
+
flushTimer = setTimeout(flush, 2000);
|
|
54
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { measureAvgCharWidth } from './measureFont';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'newtone:font-metrics:v1';
|
|
5
|
+
const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
6
|
+
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
readonly ratio: number;
|
|
9
|
+
readonly measuredAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FontMetricsCache = Record<string, CacheEntry>;
|
|
13
|
+
|
|
14
|
+
function readCache(): FontMetricsCache {
|
|
15
|
+
if (typeof localStorage === 'undefined') return {};
|
|
16
|
+
try {
|
|
17
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
18
|
+
return raw ? (JSON.parse(raw) as FontMetricsCache) : {};
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeCache(cache: FontMetricsCache): void {
|
|
25
|
+
if (typeof localStorage === 'undefined') return;
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
|
|
28
|
+
} catch {
|
|
29
|
+
// Ignore write errors (quota exceeded, private browsing)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cacheKey(fontFamily: string, fontWeight: number): string {
|
|
34
|
+
return `${fontFamily}:${fontWeight}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the avgCharWidth ratio for a font, using a device-local measurement
|
|
39
|
+
* cached in localStorage (Layer 2 calibration).
|
|
40
|
+
*
|
|
41
|
+
* Resolution order:
|
|
42
|
+
* 1. localStorage cache (if fresh, < 7 days old)
|
|
43
|
+
* 2. `baseCalibration` (Layer 1 — editor-time canvas measurement from theme config)
|
|
44
|
+
* 3. 0.55 (fallback constant)
|
|
45
|
+
*
|
|
46
|
+
* On mount, measures the font if the cache is stale or absent, then updates
|
|
47
|
+
* localStorage and triggers a re-render with the device-accurate ratio.
|
|
48
|
+
*
|
|
49
|
+
* @param fontFamily - CSS font family name, e.g. "Inter"
|
|
50
|
+
* @param fontWeight - CSS font-weight number, e.g. 400
|
|
51
|
+
* @param fallback - CSS fallback stack, e.g. "sans-serif"
|
|
52
|
+
* @param baseCalibration - Layer 1 ratio from `themeConfig.typography.calibrations`
|
|
53
|
+
*/
|
|
54
|
+
export function useLocalCalibration(
|
|
55
|
+
fontFamily: string,
|
|
56
|
+
fontWeight: number,
|
|
57
|
+
fallback: string,
|
|
58
|
+
baseCalibration?: number,
|
|
59
|
+
): number {
|
|
60
|
+
const key = cacheKey(fontFamily, fontWeight);
|
|
61
|
+
|
|
62
|
+
// Initialise synchronously from cache so the first render uses a calibrated ratio
|
|
63
|
+
const [ratio, setRatio] = useState<number>(() => {
|
|
64
|
+
const cache = readCache();
|
|
65
|
+
const entry = cache[key];
|
|
66
|
+
if (entry && Date.now() - entry.measuredAt < TTL_MS) {
|
|
67
|
+
return entry.ratio;
|
|
68
|
+
}
|
|
69
|
+
return baseCalibration ?? 0.55;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const cache = readCache();
|
|
74
|
+
const entry = cache[key];
|
|
75
|
+
if (entry && Date.now() - entry.measuredAt < TTL_MS) {
|
|
76
|
+
// Cache is fresh — no re-measurement needed
|
|
77
|
+
if (entry.ratio !== ratio) setRatio(entry.ratio);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cache is stale or absent — measure in the background
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
measureAvgCharWidth(fontFamily, fontWeight, fallback).then((measured) => {
|
|
84
|
+
if (cancelled) return;
|
|
85
|
+
const updated = { ...readCache(), [key]: { ratio: measured, measuredAt: Date.now() } };
|
|
86
|
+
writeCache(updated);
|
|
87
|
+
setRatio(measured);
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
};
|
|
92
|
+
// Intentionally excluding `ratio` from deps — we only want to re-measure on key/font change
|
|
93
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
|
+
}, [key, fontFamily, fontWeight, fallback]);
|
|
95
|
+
|
|
96
|
+
return ratio;
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useNewtoneTheme } from '../theme/NewtoneProvider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the per-font avgCharWidth calibrations from the current theme config.
|
|
5
|
+
*
|
|
6
|
+
* These are Layer 1 (editor-time) calibrations — measured with canvas at publish
|
|
7
|
+
* time and stored in `themeConfig.typography.calibrations`. Use `useLocalCalibration`
|
|
8
|
+
* to overlay device-local (Layer 2) measurements on top.
|
|
9
|
+
*
|
|
10
|
+
* @returns Record of fontFamily → avgCharWidthRatio, or an empty object if absent.
|
|
11
|
+
*/
|
|
12
|
+
export function useTypographyCalibrations(): Record<string, number> {
|
|
13
|
+
const { config } = useNewtoneTheme();
|
|
14
|
+
return config.typography.calibrations ?? {};
|
|
15
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,18 @@ export type {
|
|
|
8
8
|
NewtoneThemeConfig,
|
|
9
9
|
NewtoneThemeContext,
|
|
10
10
|
FontConfig,
|
|
11
|
+
FontWeights,
|
|
12
|
+
FontSlot,
|
|
11
13
|
TokenOverrides,
|
|
14
|
+
FontSizeScale,
|
|
15
|
+
LineHeightScale,
|
|
16
|
+
RoleSizeStep,
|
|
17
|
+
RoleScale,
|
|
18
|
+
RoleScales,
|
|
12
19
|
} from './theme/types';
|
|
13
20
|
export { useFrameContext } from './theme/FrameContext';
|
|
14
21
|
export type { FrameContextValue } from './theme/FrameContext';
|
|
15
|
-
export { DEFAULT_THEME_CONFIG } from './theme/defaults';
|
|
22
|
+
export { DEFAULT_THEME_CONFIG, DEFAULT_FONT_SIZES, DEFAULT_LINE_HEIGHTS, DEFAULT_ROLE_SCALES } from './theme/defaults';
|
|
16
23
|
|
|
17
24
|
// Tokens
|
|
18
25
|
export { useTokens } from './tokens/useTokens';
|
|
@@ -88,16 +95,17 @@ export type { IconProps } from './primitives/Icon/Icon.types';
|
|
|
88
95
|
export { Wrapper } from './primitives/Wrapper/Wrapper';
|
|
89
96
|
export type { WrapperProps } from './primitives/Wrapper/Wrapper.types';
|
|
90
97
|
|
|
91
|
-
export { Text } from './primitives/Text
|
|
98
|
+
export { Text } from './primitives/Text';
|
|
92
99
|
export type {
|
|
93
100
|
TextProps,
|
|
94
101
|
TextSize,
|
|
95
102
|
TextWeight,
|
|
96
103
|
TextColor,
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
TextScope,
|
|
105
|
+
TextRole,
|
|
99
106
|
TextAlign,
|
|
100
|
-
|
|
107
|
+
TextSpanProps,
|
|
108
|
+
} from './primitives/Text';
|
|
101
109
|
|
|
102
110
|
export { AppShell } from './composites/layout/AppShell/AppShell';
|
|
103
111
|
export type { AppShellProps } from './composites/layout/AppShell/AppShell.types';
|
|
@@ -115,6 +123,7 @@ export type {
|
|
|
115
123
|
EditableProp,
|
|
116
124
|
EditablePropOption,
|
|
117
125
|
ComponentMeta,
|
|
126
|
+
IconCatalogCategory,
|
|
118
127
|
} from './registry';
|
|
119
128
|
export {
|
|
120
129
|
CATEGORIES,
|
|
@@ -123,12 +132,14 @@ export {
|
|
|
123
132
|
getCategory,
|
|
124
133
|
getComponentsByCategory,
|
|
125
134
|
generateComponentCode,
|
|
135
|
+
ICON_CATALOG,
|
|
126
136
|
} from './registry';
|
|
127
137
|
|
|
128
138
|
// Fonts
|
|
129
|
-
export { GOOGLE_FONTS, SYSTEM_FONTS } from './fonts/googleFonts';
|
|
130
|
-
export type { GoogleFontEntry, SystemFontEntry } from './fonts/googleFonts';
|
|
131
139
|
export { buildGoogleFontsUrl } from './fonts/buildGoogleFontsUrl';
|
|
140
|
+
export { measureAvgCharWidth } from './fonts/measureFont';
|
|
141
|
+
export { useLocalCalibration } from './fonts/useLocalCalibration';
|
|
142
|
+
export { useTypographyCalibrations } from './fonts/useTypographyCalibrations';
|
|
132
143
|
|
|
133
144
|
// Re-export core engine types for convenience
|
|
134
145
|
export type {
|
|
@@ -246,9 +246,9 @@ export function Frame({
|
|
|
246
246
|
const textStyle = useMemo<TextStyle>(
|
|
247
247
|
() => ({
|
|
248
248
|
color: srgbToHex(tokens.textPrimary.srgb),
|
|
249
|
-
fontSize: tokens.typography.
|
|
250
|
-
fontFamily: tokens.typography.fonts.
|
|
251
|
-
lineHeight: tokens.typography.
|
|
249
|
+
fontSize: tokens.typography.fontSizes['05'],
|
|
250
|
+
fontFamily: tokens.typography.fonts.main.family,
|
|
251
|
+
lineHeight: tokens.typography.lineHeights['06'],
|
|
252
252
|
}),
|
|
253
253
|
[tokens],
|
|
254
254
|
);
|
|
@@ -23,9 +23,7 @@ export function Icon({
|
|
|
23
23
|
opticalSize,
|
|
24
24
|
fill = 0,
|
|
25
25
|
color,
|
|
26
|
-
elevation = 1,
|
|
27
26
|
style,
|
|
28
|
-
onPress,
|
|
29
27
|
// Accessibility
|
|
30
28
|
accessibilityLabel,
|
|
31
29
|
// Testing & platform
|
|
@@ -33,15 +31,15 @@ export function Icon({
|
|
|
33
31
|
nativeID,
|
|
34
32
|
ref,
|
|
35
33
|
}: IconProps) {
|
|
36
|
-
//
|
|
37
|
-
const tokens = useTokens(
|
|
34
|
+
// Inherit tokens from nearest parent Frame via FrameContext.
|
|
35
|
+
const tokens = useTokens();
|
|
38
36
|
|
|
39
37
|
// Build the icon's style from the theme tokens and props.
|
|
40
38
|
// Wrapped in useMemo so it only recalculates when the inputs change,
|
|
41
39
|
// instead of rebuilding the style object on every render.
|
|
42
40
|
const iconStyle = useMemo<TextStyle>(() => {
|
|
43
41
|
// Use the provided size, or fall back to the theme's default text size.
|
|
44
|
-
const fontSize = size ?? tokens.typography.
|
|
42
|
+
const fontSize = size ?? tokens.typography.fontSizes['05'];
|
|
45
43
|
|
|
46
44
|
// Round to nearest Material Symbols optical size (20, 24, 40, 48)
|
|
47
45
|
// for optimal stroke weight and detail rendering.
|
|
@@ -69,7 +67,8 @@ export function Icon({
|
|
|
69
67
|
// wght: font weight (thinner or bolder strokes)
|
|
70
68
|
// GRAD: grade (fine-tune weight without changing overall size)
|
|
71
69
|
// opsz: optical size (adjusts detail level for the display size)
|
|
72
|
-
const
|
|
70
|
+
const fillValue = typeof fill === 'boolean' ? (fill ? 1 : 0) : fill;
|
|
71
|
+
const fontVariationSettings = `'FILL' ${fillValue}, 'wght' ${tokens.icons.weight}, 'GRAD' ${tokens.icons.grade}, 'opsz' ${opsz}`;
|
|
73
72
|
|
|
74
73
|
return {
|
|
75
74
|
fontFamily,
|
|
@@ -96,7 +95,6 @@ export function Icon({
|
|
|
96
95
|
// so assistive technology doesn't try to read the ligature text (e.g. "home").
|
|
97
96
|
importantForAccessibility={accessibilityLabel ? 'yes' : 'no-hide-descendants'}
|
|
98
97
|
style={iconStyle}
|
|
99
|
-
onPress={onPress} // When provided, makes the icon tappable
|
|
100
98
|
>
|
|
101
99
|
{name}
|
|
102
100
|
</Text>
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import type { Text as RNText, TextStyle } from 'react-native';
|
|
2
|
-
import type { ElevationLevel } from '../../theme/types';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Props for Icon — Material Symbols icon with variable font support.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Icon is a pure presentation primitive. It inherits elevation from the
|
|
7
|
+
* nearest parent Frame via FrameContext. For interactive icons, wrap in
|
|
8
|
+
* `<Button icon="..." />` instead.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```tsx
|
|
12
12
|
* <Icon name="home" />
|
|
13
13
|
* <Icon name="settings" size={24} fill={1} />
|
|
14
14
|
* <Icon name="check" color="#00ff00" />
|
|
15
|
-
* <Icon name="delete" size={20} onPress={() => handleDelete()} />
|
|
16
15
|
* ```
|
|
17
16
|
*
|
|
18
17
|
* @see {@link https://fonts.google.com/icons Material Symbols icon reference}
|
|
@@ -26,7 +25,7 @@ export interface IconProps {
|
|
|
26
25
|
*/
|
|
27
26
|
readonly name: string;
|
|
28
27
|
|
|
29
|
-
/** Font size in pixels. @default tokens.typography.
|
|
28
|
+
/** Font size in pixels. @default tokens.typography.fontSizes['05'] (16px) */
|
|
30
29
|
readonly size?: number;
|
|
31
30
|
|
|
32
31
|
/** Optical size for variable font axis. Adjusts stroke weight for readability at small sizes. @default same as size */
|
|
@@ -35,23 +34,17 @@ export interface IconProps {
|
|
|
35
34
|
/**
|
|
36
35
|
* Fill state for the icon.
|
|
37
36
|
*
|
|
38
|
-
* - `0` — Outlined (default)
|
|
39
|
-
* - `1` — Filled (solid)
|
|
37
|
+
* - `false` / `0` — Outlined (default)
|
|
38
|
+
* - `true` / `1` — Filled (solid)
|
|
40
39
|
*/
|
|
41
|
-
readonly fill?: 0 | 1;
|
|
40
|
+
readonly fill?: boolean | 0 | 1;
|
|
42
41
|
|
|
43
42
|
/** Icon color as hex string. @default tokens.textPrimary */
|
|
44
43
|
readonly color?: string;
|
|
45
44
|
|
|
46
|
-
/**
|
|
47
|
-
readonly elevation?: ElevationLevel;
|
|
48
|
-
|
|
49
|
-
/** Additional styles applied to the icon Text element. */
|
|
45
|
+
/** Additional styles applied to the icon Text element (positioning, transforms). */
|
|
50
46
|
readonly style?: TextStyle;
|
|
51
47
|
|
|
52
|
-
/** Press handler — makes the icon tappable. */
|
|
53
|
-
readonly onPress?: () => void;
|
|
54
|
-
|
|
55
48
|
// ── Accessibility ──
|
|
56
49
|
|
|
57
50
|
/** Label read by screen readers. When omitted, the icon is treated as decorative (hidden from assistive technology). */
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import { Text as RNText } from 'react-native';
|
|
3
|
+
import type { TextStyle } from 'react-native';
|
|
4
|
+
import { useTokens } from '../../tokens/useTokens';
|
|
5
|
+
import { TextScopeContext, resolveTextColor } from './Text';
|
|
6
|
+
import type { TextSpanProps, TextWeight } from './Text.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generic inline span — constrained sub-component for inline text formatting.
|
|
10
|
+
* Inherits font family, size, and line height from the parent Text.
|
|
11
|
+
* Only inline formatting properties (color, weight, italic, underline, highlight) are exposed.
|
|
12
|
+
*/
|
|
13
|
+
export function TextSpan({ children, color, weight, italic, underline, highlight, style }: TextSpanProps) {
|
|
14
|
+
const tokens = useTokens(1);
|
|
15
|
+
const scopeCtx = useContext(TextScopeContext);
|
|
16
|
+
|
|
17
|
+
const spanStyle = useMemo<TextStyle>(() => {
|
|
18
|
+
const s: TextStyle = {};
|
|
19
|
+
if (color) s.color = resolveTextColor(color, tokens);
|
|
20
|
+
if (weight && scopeCtx) s.fontWeight = String(scopeCtx.weights[weight]) as TextStyle['fontWeight'];
|
|
21
|
+
if (italic) s.fontStyle = 'italic';
|
|
22
|
+
if (underline) s.textDecorationLine = 'underline';
|
|
23
|
+
if (highlight) s.backgroundColor = resolveTextColor(highlight, tokens);
|
|
24
|
+
return s;
|
|
25
|
+
}, [tokens, scopeCtx, color, weight, italic, underline, highlight]);
|
|
26
|
+
|
|
27
|
+
return React.createElement(
|
|
28
|
+
RNText,
|
|
29
|
+
{ style: style ? [spanStyle, ...(Array.isArray(style) ? style : [style])] : spanStyle },
|
|
30
|
+
children,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Bold span — applies bold weight inline. */
|
|
35
|
+
export function TextBold(props: Omit<TextSpanProps, 'weight'>) {
|
|
36
|
+
return React.createElement(TextSpan, { ...props, weight: 'bold' as TextWeight });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Medium-weight span — applies medium weight inline. */
|
|
40
|
+
export function TextMedium(props: Omit<TextSpanProps, 'weight'>) {
|
|
41
|
+
return React.createElement(TextSpan, { ...props, weight: 'medium' as TextWeight });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Italic span — applies italic style inline. */
|
|
45
|
+
export function TextItalic(props: Omit<TextSpanProps, 'italic'>) {
|
|
46
|
+
return React.createElement(TextSpan, { ...props, italic: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Underline span — applies underline decoration inline. */
|
|
50
|
+
export function TextUnderline(props: Omit<TextSpanProps, 'underline'>) {
|
|
51
|
+
return React.createElement(TextSpan, { ...props, underline: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Highlight span — applies background highlight using a semantic color token. */
|
|
55
|
+
export function TextHighlight(props: Omit<TextSpanProps, 'highlight'> & { readonly highlight: TextSpanProps['highlight'] }) {
|
|
56
|
+
return React.createElement(TextSpan, props as TextSpanProps);
|
|
57
|
+
}
|