@ankhorage/zora 3.1.0 → 3.2.0

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 (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/components/card/meta.d.ts +1 -1
  4. package/dist/components/progress/ProgressRing.d.ts +7 -0
  5. package/dist/components/progress/ProgressRing.d.ts.map +1 -0
  6. package/dist/components/progress/ProgressRing.js +100 -0
  7. package/dist/components/progress/ProgressRing.js.map +1 -0
  8. package/dist/components/progress/index.d.ts +2 -1
  9. package/dist/components/progress/index.d.ts.map +1 -1
  10. package/dist/components/progress/index.js +1 -0
  11. package/dist/components/progress/index.js.map +1 -1
  12. package/dist/components/progress/meta.d.ts +116 -0
  13. package/dist/components/progress/meta.d.ts.map +1 -1
  14. package/dist/components/progress/meta.js +97 -0
  15. package/dist/components/progress/meta.js.map +1 -1
  16. package/dist/components/progress/resolveProgressRingGeometry.d.ts +12 -0
  17. package/dist/components/progress/resolveProgressRingGeometry.d.ts.map +1 -0
  18. package/dist/components/progress/resolveProgressRingGeometry.js +27 -0
  19. package/dist/components/progress/resolveProgressRingGeometry.js.map +1 -0
  20. package/dist/components/progress/types.d.ts +12 -0
  21. package/dist/components/progress/types.d.ts.map +1 -1
  22. package/dist/components/progress/types.js.map +1 -1
  23. package/dist/foundation/meta.d.ts +4 -4
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/layout/screen/meta.d.ts +1 -1
  29. package/dist/layout/screen-section/meta.d.ts +1 -1
  30. package/dist/metadata/allowedChildren.d.ts +3 -3
  31. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  32. package/dist/metadata/allowedChildren.js +1 -0
  33. package/dist/metadata/allowedChildren.js.map +1 -1
  34. package/dist/metadata/bindableComponentMeta.d.ts +2 -2
  35. package/dist/metadata/componentMeta.d.ts.map +1 -1
  36. package/dist/metadata/componentMeta.js +2 -1
  37. package/dist/metadata/componentMeta.js.map +1 -1
  38. package/dist/patterns/message-bubble/meta.d.ts +1 -1
  39. package/dist/patterns/notice/meta.d.ts +1 -1
  40. package/dist/patterns/panel/meta.d.ts +1 -1
  41. package/dist/patterns/post-card/meta.d.ts +1 -1
  42. package/dist/patterns/reader/meta.d.ts +2 -2
  43. package/dist/patterns/scanner/meta.d.ts +2 -2
  44. package/dist/registry.d.ts.map +1 -1
  45. package/dist/registry.js +2 -1
  46. package/dist/registry.js.map +1 -1
  47. package/dist/theme/useZoraTheme.d.ts +1 -1
  48. package/package.json +2 -2
  49. package/src/components/progress/ProgressRing.test.ts +48 -0
  50. package/src/components/progress/ProgressRing.tsx +151 -0
  51. package/src/components/progress/index.ts +2 -1
  52. package/src/components/progress/meta.ts +99 -0
  53. package/src/components/progress/resolveProgressRingGeometry.test.ts +30 -0
  54. package/src/components/progress/resolveProgressRingGeometry.ts +44 -0
  55. package/src/components/progress/types.ts +13 -0
  56. package/src/index.ts +2 -2
  57. package/src/metadata/allowedChildren.ts +1 -0
  58. package/src/metadata/componentMeta.test.ts +17 -0
  59. package/src/metadata/componentMeta.ts +2 -1
  60. package/src/registry.ts +2 -1
  61. package/src/showcaseCoverage.test.ts +1 -0
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { progressRingMeta } from './meta';
4
+
5
+ describe('ProgressRing public contract', () => {
6
+ test('is exported from the component and package entrypoints', async () => {
7
+ const [componentIndex, rootIndex] = await Promise.all([
8
+ Bun.file('src/components/progress/index.ts').text(),
9
+ Bun.file('src/index.ts').text(),
10
+ ]);
11
+
12
+ expect(componentIndex).toContain("export { ProgressRing } from './ProgressRing';");
13
+ expect(componentIndex).toContain('ProgressRingProps');
14
+ expect(rootIndex).toContain("export { Progress, ProgressRing } from './components/progress';");
15
+ expect(rootIndex).toContain('ProgressProps, ProgressRingProps');
16
+ });
17
+
18
+ test('exposes native progress semantics and uses canonical normalization and colors', async () => {
19
+ const source = await Bun.file('src/components/progress/ProgressRing.tsx').text();
20
+
21
+ expect(source).toContain('accessibilityRole="progressbar"');
22
+ expect(source).toContain('accessibilityValue={{');
23
+ expect(source).toContain('resolveProgressFraction({ max, value })');
24
+ expect(source).toContain('resolveProgressRole(theme, color)');
25
+ expect(source).not.toMatch(/#[0-9A-Fa-f]{3,8}|rgba?\(|hsla?\(/);
26
+ });
27
+
28
+ test('declares a complete serializable manifest blueprint', () => {
29
+ expect(progressRingMeta.directManifestNode).toBe(true);
30
+ expect(progressRingMeta.allowedChildren).toEqual([]);
31
+ expect(progressRingMeta.blueprint.defaultProps).toEqual({
32
+ value: 68,
33
+ max: 100,
34
+ color: 'primary',
35
+ trackColor: 'neutral',
36
+ size: 120,
37
+ thickness: 10,
38
+ centerValue: '68%',
39
+ centerLabel: 'Complete',
40
+ accessibilityLabel: 'Progress',
41
+ accessibilityValueText: '68% complete',
42
+ });
43
+ expect(Object.keys(progressRingMeta.props).sort()).toEqual(
44
+ Object.keys(progressRingMeta.blueprint.defaultProps).sort(),
45
+ );
46
+ expect(JSON.parse(JSON.stringify(progressRingMeta))).toEqual(progressRingMeta);
47
+ });
48
+ });
@@ -0,0 +1,151 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ import { Stack } from '../../foundation';
5
+ import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
+ import { Text } from '../text';
8
+ import { resolveProgressFraction } from './resolveProgressFraction';
9
+ import { resolveProgressRingGeometry } from './resolveProgressRingGeometry';
10
+ import { type ProgressRingProps, resolveProgressRole } from './types';
11
+
12
+ function ProgressRingInner({
13
+ themeId: _themeId,
14
+ mode: _mode,
15
+ interactionPolicy: _interactionPolicy,
16
+ accessibilityLabel = 'Progress',
17
+ accessibilityValueText,
18
+ centerLabel,
19
+ centerValue,
20
+ color = 'primary',
21
+ max = 100,
22
+ size = 120,
23
+ testID,
24
+ thickness = 10,
25
+ trackColor = 'neutral',
26
+ value,
27
+ }: ProgressRingProps) {
28
+ const { theme } = useZoraTheme();
29
+ const fraction = resolveProgressFraction({ max, value });
30
+ const geometry = resolveProgressRingGeometry({ fraction, size, thickness });
31
+ const progressRole = resolveProgressRole(theme, color);
32
+ const trackRole = resolveProgressRole(theme, trackColor);
33
+ const accessibilityMaximum = Number.isFinite(max) && max > 0 ? max : 1;
34
+ const accessibilityValue = fraction * accessibilityMaximum;
35
+ const valueText = accessibilityValueText ?? `${Math.round(fraction * 100)}%`;
36
+ const segments = Array.from({ length: geometry.filledSegmentCount });
37
+
38
+ return (
39
+ <View
40
+ accessibilityLabel={accessibilityLabel}
41
+ accessibilityRole="progressbar"
42
+ accessibilityValue={{
43
+ max: accessibilityMaximum,
44
+ min: 0,
45
+ now: accessibilityValue,
46
+ text: valueText,
47
+ }}
48
+ accessible
49
+ style={{ height: geometry.diameter, width: geometry.diameter }}
50
+ testID={testID}
51
+ >
52
+ <View
53
+ importantForAccessibility="no-hide-descendants"
54
+ pointerEvents="none"
55
+ style={styles.visual}
56
+ >
57
+ <View
58
+ style={[
59
+ styles.track,
60
+ {
61
+ borderColor: trackRole.softBg,
62
+ borderRadius: geometry.diameter / 2,
63
+ borderWidth: geometry.strokeWidth,
64
+ },
65
+ ]}
66
+ />
67
+
68
+ {segments.map((_, index) => (
69
+ <View
70
+ key={index}
71
+ style={[
72
+ styles.segmentLayer,
73
+ { transform: [{ rotate: `${index * (360 / geometry.segmentCount)}deg` }] },
74
+ ]}
75
+ >
76
+ <View
77
+ style={{
78
+ backgroundColor: progressRole.base,
79
+ borderRadius: geometry.strokeWidth / 2,
80
+ height: geometry.strokeWidth,
81
+ left: (geometry.diameter - geometry.segmentWidth) / 2,
82
+ position: 'absolute',
83
+ top: 0,
84
+ width: geometry.segmentWidth,
85
+ }}
86
+ />
87
+ </View>
88
+ ))}
89
+ </View>
90
+
91
+ {centerValue !== undefined || centerLabel !== undefined ? (
92
+ <View
93
+ importantForAccessibility="no-hide-descendants"
94
+ pointerEvents="none"
95
+ style={styles.center}
96
+ >
97
+ <Stack align="center" gap="xxs" px="s">
98
+ {centerValue !== undefined ? (
99
+ <Text align="center" variant="lead" weight="bold">
100
+ {centerValue}
101
+ </Text>
102
+ ) : null}
103
+ {centerLabel !== undefined ? (
104
+ <Text align="center" emphasis="muted" numberOfLines={2} variant="caption">
105
+ {centerLabel}
106
+ </Text>
107
+ ) : null}
108
+ </Stack>
109
+ </View>
110
+ ) : null}
111
+ </View>
112
+ );
113
+ }
114
+
115
+ const styles = StyleSheet.create({
116
+ center: {
117
+ alignItems: 'center',
118
+ bottom: 0,
119
+ justifyContent: 'center',
120
+ left: 0,
121
+ position: 'absolute',
122
+ right: 0,
123
+ top: 0,
124
+ },
125
+ segmentLayer: {
126
+ bottom: 0,
127
+ left: 0,
128
+ position: 'absolute',
129
+ right: 0,
130
+ top: 0,
131
+ },
132
+ track: {
133
+ bottom: 0,
134
+ left: 0,
135
+ position: 'absolute',
136
+ right: 0,
137
+ top: 0,
138
+ },
139
+ visual: {
140
+ bottom: 0,
141
+ left: 0,
142
+ position: 'absolute',
143
+ right: 0,
144
+ top: 0,
145
+ },
146
+ });
147
+
148
+ /***
149
+ * Circular determinate progress with optional serializable center value and label content.
150
+ */
151
+ export const ProgressRing = withZoraThemeScope(ProgressRingInner);
@@ -1,2 +1,3 @@
1
1
  export { Progress } from './Progress';
2
- export type { ProgressProps } from './types';
2
+ export { ProgressRing } from './ProgressRing';
3
+ export type { ProgressProps, ProgressRingProps } from './types';
@@ -44,3 +44,102 @@ export const progressMeta = {
44
44
  },
45
45
  },
46
46
  } as const satisfies ZoraComponentMeta;
47
+
48
+ const instanceAuthoring = { authority: 'instance' } as const;
49
+
50
+ export const progressRingMeta = {
51
+ name: 'ProgressRing',
52
+ category: 'component',
53
+ description: 'Displays circular determinate progress with optional center value and label text.',
54
+ directManifestNode: true,
55
+ allowedChildren: [],
56
+ blueprint: {
57
+ label: 'Progress ring',
58
+ defaultProps: {
59
+ value: 68,
60
+ max: 100,
61
+ color: 'primary',
62
+ trackColor: 'neutral',
63
+ size: 120,
64
+ thickness: 10,
65
+ centerValue: '68%',
66
+ centerLabel: 'Complete',
67
+ accessibilityLabel: 'Progress',
68
+ accessibilityValueText: '68% complete',
69
+ },
70
+ },
71
+ props: {
72
+ value: {
73
+ type: 'number',
74
+ category: 'State',
75
+ label: 'Value',
76
+ default: 68,
77
+ authoring: instanceAuthoring,
78
+ },
79
+ max: {
80
+ type: 'number',
81
+ category: 'State',
82
+ label: 'Maximum',
83
+ default: 100,
84
+ authoring: instanceAuthoring,
85
+ },
86
+ centerValue: {
87
+ type: 'string',
88
+ category: 'Content',
89
+ label: 'Center value',
90
+ default: '68%',
91
+ authoring: instanceAuthoring,
92
+ },
93
+ centerLabel: {
94
+ type: 'string',
95
+ category: 'Content',
96
+ label: 'Center label',
97
+ default: 'Complete',
98
+ authoring: instanceAuthoring,
99
+ },
100
+ accessibilityLabel: {
101
+ type: 'string',
102
+ category: 'Accessibility',
103
+ label: 'Accessibility label',
104
+ default: 'Progress',
105
+ authoring: instanceAuthoring,
106
+ },
107
+ accessibilityValueText: {
108
+ type: 'string',
109
+ category: 'Accessibility',
110
+ label: 'Accessibility value text',
111
+ default: '68% complete',
112
+ authoring: instanceAuthoring,
113
+ },
114
+ color: {
115
+ type: 'enum',
116
+ category: 'Style',
117
+ label: 'Progress color',
118
+ enum: [...ZORA_COLORS],
119
+ default: 'primary',
120
+ authoring: instanceAuthoring,
121
+ },
122
+ trackColor: {
123
+ type: 'enum',
124
+ category: 'Style',
125
+ label: 'Track color',
126
+ enum: [...ZORA_COLORS],
127
+ default: 'neutral',
128
+ authoring: instanceAuthoring,
129
+ },
130
+ size: {
131
+ type: 'number',
132
+ category: 'Layout',
133
+ label: 'Diameter',
134
+ default: 120,
135
+ authoring: instanceAuthoring,
136
+ },
137
+ thickness: {
138
+ type: 'number',
139
+ category: 'Style',
140
+ label: 'Thickness',
141
+ default: 10,
142
+ authoring: instanceAuthoring,
143
+ },
144
+ },
145
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveProgressRingGeometry } from './resolveProgressRingGeometry';
4
+
5
+ describe('resolveProgressRingGeometry', () => {
6
+ test('normalizes invalid dimensions and fractions deterministically', () => {
7
+ expect(
8
+ resolveProgressRingGeometry({ fraction: Number.NaN, size: Number.NaN, thickness: -1 }),
9
+ ).toMatchObject({
10
+ diameter: 120,
11
+ filledSegmentCount: 0,
12
+ strokeWidth: 10,
13
+ });
14
+ });
15
+
16
+ test('clamps size, thickness, and progress to renderable bounds', () => {
17
+ const empty = resolveProgressRingGeometry({ fraction: -1, size: 4, thickness: 30 });
18
+ const complete = resolveProgressRingGeometry({ fraction: 2, size: 80, thickness: 8 });
19
+
20
+ expect(empty).toMatchObject({ diameter: 24, filledSegmentCount: 0, strokeWidth: 12 });
21
+ expect(complete.filledSegmentCount).toBe(complete.segmentCount);
22
+ });
23
+
24
+ test('maps determinate progress to the ring segments', () => {
25
+ const geometry = resolveProgressRingGeometry({ fraction: 0.68, size: 120, thickness: 10 });
26
+
27
+ expect(geometry.filledSegmentCount).toBe(Math.round(geometry.segmentCount * 0.68));
28
+ expect(geometry.segmentWidth).toBeGreaterThan(0);
29
+ });
30
+ });
@@ -0,0 +1,44 @@
1
+ export function resolveProgressRingGeometry({
2
+ fraction,
3
+ size,
4
+ thickness,
5
+ }: {
6
+ fraction: number;
7
+ size: number;
8
+ thickness: number;
9
+ }) {
10
+ const diameter = normalizePositiveNumber(size, DEFAULT_SIZE, MINIMUM_SIZE);
11
+ const strokeWidth = Math.min(
12
+ normalizePositiveNumber(thickness, DEFAULT_THICKNESS, MINIMUM_THICKNESS),
13
+ diameter / 2,
14
+ );
15
+ const centerlineRadius = (diameter - strokeWidth) / 2;
16
+ const segmentCount = Math.max(
17
+ MINIMUM_SEGMENT_COUNT,
18
+ Math.min(MAXIMUM_SEGMENT_COUNT, Math.round(Math.PI * centerlineRadius)),
19
+ );
20
+ const normalizedFraction = Math.min(Math.max(Number.isFinite(fraction) ? fraction : 0, 0), 1);
21
+
22
+ return {
23
+ diameter,
24
+ filledSegmentCount: Math.round(normalizedFraction * segmentCount),
25
+ segmentCount,
26
+ segmentWidth: Math.max(1, (2 * Math.PI * centerlineRadius) / segmentCount + 0.5),
27
+ strokeWidth,
28
+ };
29
+ }
30
+
31
+ const DEFAULT_SIZE = 120;
32
+ const DEFAULT_THICKNESS = 10;
33
+ const MAXIMUM_SEGMENT_COUNT = 120;
34
+ const MINIMUM_SEGMENT_COUNT = 32;
35
+ const MINIMUM_SIZE = 24;
36
+ const MINIMUM_THICKNESS = 1;
37
+
38
+ function normalizePositiveNumber(value: number, fallback: number, minimum: number): number {
39
+ if (!Number.isFinite(value) || value <= 0) {
40
+ return fallback;
41
+ }
42
+
43
+ return Math.max(value, minimum);
44
+ }
@@ -10,6 +10,19 @@ export interface ProgressProps extends ZoraBaseProps {
10
10
  size?: ZoraControlSize;
11
11
  }
12
12
 
13
+ export interface ProgressRingProps extends ZoraBaseProps {
14
+ value: number;
15
+ max?: number;
16
+ color?: ZoraColor;
17
+ trackColor?: ZoraColor;
18
+ size?: number;
19
+ thickness?: number;
20
+ centerValue?: string;
21
+ centerLabel?: string;
22
+ accessibilityLabel?: string;
23
+ accessibilityValueText?: string;
24
+ }
25
+
13
26
  export function resolveProgressRole(theme: SurfaceTheme, color: ZoraColor): RoleSemantics {
14
27
  switch (color) {
15
28
  case 'secondary':
package/src/index.ts CHANGED
@@ -115,8 +115,8 @@ export type { NavigationListProps, ZoraNavigationRouteMap } from './components/n
115
115
  export { NavigationList } from './components/navigation-list';
116
116
  export type { PaginationProps } from './components/pagination';
117
117
  export { Pagination } from './components/pagination';
118
- export type { ProgressProps } from './components/progress';
119
- export { Progress } from './components/progress';
118
+ export type { ProgressProps, ProgressRingProps } from './components/progress';
119
+ export { Progress, ProgressRing } from './components/progress';
120
120
  export type { RadioGroupOption, RadioGroupProps, RadioProps } from './components/radio';
121
121
  export { Radio, RadioGroup } from './components/radio';
122
122
  export type { RatingProps } from './components/rating';
@@ -21,6 +21,7 @@ export const CONTAINER_ALLOWED_CHILDREN = [
21
21
  'PostCard',
22
22
  'ChatListItem',
23
23
  'MessageBubble',
24
+ 'ProgressRing',
24
25
  'ReaderSurface',
25
26
  ] as const;
26
27
 
@@ -138,6 +138,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
138
138
  'ScanOverlay',
139
139
  'ProductCard',
140
140
  'Progress',
141
+ 'ProgressRing',
141
142
  'ReaderSurface',
142
143
  ]);
143
144
 
@@ -211,6 +212,22 @@ describe('ZORA_COMPONENT_META invariants', () => {
211
212
  });
212
213
  });
213
214
 
215
+ test('ProgressRing is a direct manifest leaf with serializable center content', () => {
216
+ const progressRing = ZORA_COMPONENT_META.ProgressRing;
217
+
218
+ expect(progressRing.directManifestNode).toBe(true);
219
+ expect(progressRing.allowedChildren).toEqual([]);
220
+ expect(progressRing.blueprint?.defaultProps).toMatchObject({
221
+ centerLabel: 'Complete',
222
+ centerValue: '68%',
223
+ max: 100,
224
+ value: 68,
225
+ });
226
+ expect(progressRing.props.centerValue?.type).toBe('string');
227
+ expect(progressRing.props.centerLabel?.type).toBe('string');
228
+ expect(progressRing.props.accessibilityValueText?.type).toBe('string');
229
+ });
230
+
214
231
  test('ReaderSurface exposes a safe file-media blueprint and normalized events', () => {
215
232
  const reader = ZORA_COMPONENT_META.ReaderSurface;
216
233
 
@@ -27,7 +27,7 @@ import { modalMeta } from '../components/modal/meta';
27
27
  import { navigationItemMeta } from '../components/navigation-item/meta';
28
28
  import { navigationListMeta } from '../components/navigation-list/meta';
29
29
  import { paginationMeta } from '../components/pagination/meta';
30
- import { progressMeta } from '../components/progress/meta';
30
+ import { progressMeta, progressRingMeta } from '../components/progress/meta';
31
31
  import { radioGroupMeta, radioMeta } from '../components/radio/meta';
32
32
  import { ratingMeta } from '../components/rating/meta';
33
33
  import { searchBarMeta } from '../components/search-bar/meta';
@@ -134,6 +134,7 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = {
134
134
  NavigationList: navigationListMeta,
135
135
  Pagination: paginationMeta,
136
136
  Progress: progressMeta,
137
+ ProgressRing: progressRingMeta,
137
138
  Radio: radioMeta,
138
139
  RadioGroup: radioGroupMeta,
139
140
  Rating: ratingMeta,
package/src/registry.ts CHANGED
@@ -30,7 +30,7 @@ import { Modal } from './components/modal';
30
30
  import { NavigationItem } from './components/navigation-item';
31
31
  import { NavigationList } from './components/navigation-list';
32
32
  import { Pagination } from './components/pagination';
33
- import { Progress } from './components/progress';
33
+ import { Progress, ProgressRing } from './components/progress';
34
34
  import { Radio, RadioGroup } from './components/radio';
35
35
  import { Rating } from './components/rating';
36
36
  import { SearchBar } from './components/search-bar';
@@ -162,6 +162,7 @@ const _ZORA_COMPONENT_REGISTRY = {
162
162
  NavigationList,
163
163
  Pagination,
164
164
  Progress,
165
+ ProgressRing,
165
166
  Radio,
166
167
  RadioGroup,
167
168
  Rating,
@@ -44,6 +44,7 @@ const REQUIRED_SHOWCASE_COVERAGE = {
44
44
  'NavigationItem',
45
45
  'NavigationList',
46
46
  'Progress',
47
+ 'ProgressRing',
47
48
  'Radio',
48
49
  'RadioGroup',
49
50
  'Rating',