@ankhorage/zora 1.0.6 → 1.0.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +183 -0
  3. package/dist/components/media-card/MediaCard.d.ts +4 -0
  4. package/dist/components/media-card/MediaCard.d.ts.map +1 -0
  5. package/dist/components/media-card/MediaCard.js +64 -0
  6. package/dist/components/media-card/MediaCard.js.map +1 -0
  7. package/dist/components/media-card/index.d.ts +3 -0
  8. package/dist/components/media-card/index.d.ts.map +1 -0
  9. package/dist/components/media-card/index.js +2 -0
  10. package/dist/components/media-card/index.js.map +1 -0
  11. package/dist/components/media-card/types.d.ts +36 -0
  12. package/dist/components/media-card/types.d.ts.map +1 -0
  13. package/dist/components/media-card/types.js +2 -0
  14. package/dist/components/media-card/types.js.map +1 -0
  15. package/dist/components/metric-card/MetricCard.d.ts +4 -0
  16. package/dist/components/metric-card/MetricCard.d.ts.map +1 -0
  17. package/dist/components/metric-card/MetricCard.js +43 -0
  18. package/dist/components/metric-card/MetricCard.js.map +1 -0
  19. package/dist/components/metric-card/index.d.ts +3 -0
  20. package/dist/components/metric-card/index.d.ts.map +1 -0
  21. package/dist/components/metric-card/index.js +2 -0
  22. package/dist/components/metric-card/index.js.map +1 -0
  23. package/dist/components/metric-card/types.d.ts +17 -0
  24. package/dist/components/metric-card/types.d.ts.map +1 -0
  25. package/dist/components/metric-card/types.js +2 -0
  26. package/dist/components/metric-card/types.js.map +1 -0
  27. package/dist/components/progress/Progress.d.ts +4 -0
  28. package/dist/components/progress/Progress.d.ts.map +1 -0
  29. package/dist/components/progress/Progress.js +28 -0
  30. package/dist/components/progress/Progress.js.map +1 -0
  31. package/dist/components/progress/index.d.ts +3 -0
  32. package/dist/components/progress/index.d.ts.map +1 -0
  33. package/dist/components/progress/index.js +2 -0
  34. package/dist/components/progress/index.js.map +1 -0
  35. package/dist/components/progress/resolveProgressFraction.d.ts +5 -0
  36. package/dist/components/progress/resolveProgressFraction.d.ts.map +1 -0
  37. package/dist/components/progress/resolveProgressFraction.js +14 -0
  38. package/dist/components/progress/resolveProgressFraction.js.map +1 -0
  39. package/dist/components/progress/types.d.ts +11 -0
  40. package/dist/components/progress/types.d.ts.map +1 -0
  41. package/dist/components/progress/types.js +16 -0
  42. package/dist/components/progress/types.js.map +1 -0
  43. package/dist/components/rating/Rating.d.ts +4 -0
  44. package/dist/components/rating/Rating.d.ts.map +1 -0
  45. package/dist/components/rating/Rating.js +23 -0
  46. package/dist/components/rating/Rating.js.map +1 -0
  47. package/dist/components/rating/index.d.ts +3 -0
  48. package/dist/components/rating/index.d.ts.map +1 -0
  49. package/dist/components/rating/index.js +2 -0
  50. package/dist/components/rating/index.js.map +1 -0
  51. package/dist/components/rating/resolveRatingSegments.d.ts +7 -0
  52. package/dist/components/rating/resolveRatingSegments.d.ts.map +1 -0
  53. package/dist/components/rating/resolveRatingSegments.js +22 -0
  54. package/dist/components/rating/resolveRatingSegments.js.map +1 -0
  55. package/dist/components/rating/types.d.ts +11 -0
  56. package/dist/components/rating/types.d.ts.map +1 -0
  57. package/dist/components/rating/types.js +16 -0
  58. package/dist/components/rating/types.js.map +1 -0
  59. package/dist/index.d.ts +10 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +5 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/patterns/timeline/Timeline.d.ts +4 -0
  64. package/dist/patterns/timeline/Timeline.d.ts.map +1 -0
  65. package/dist/patterns/timeline/Timeline.js +59 -0
  66. package/dist/patterns/timeline/Timeline.js.map +1 -0
  67. package/dist/patterns/timeline/index.d.ts +3 -0
  68. package/dist/patterns/timeline/index.d.ts.map +1 -0
  69. package/dist/patterns/timeline/index.js +2 -0
  70. package/dist/patterns/timeline/index.js.map +1 -0
  71. package/dist/patterns/timeline/types.d.ts +18 -0
  72. package/dist/patterns/timeline/types.d.ts.map +1 -0
  73. package/dist/patterns/timeline/types.js +2 -0
  74. package/dist/patterns/timeline/types.js.map +1 -0
  75. package/package.json +3 -3
  76. package/src/components/media-card/MediaCard.tsx +120 -0
  77. package/src/components/media-card/index.ts +2 -0
  78. package/src/components/media-card/types.ts +44 -0
  79. package/src/components/metric-card/MetricCard.tsx +84 -0
  80. package/src/components/metric-card/index.ts +2 -0
  81. package/src/components/metric-card/types.ts +18 -0
  82. package/src/components/progress/Progress.tsx +50 -0
  83. package/src/components/progress/index.ts +2 -0
  84. package/src/components/progress/resolveProgressFraction.test.ts +23 -0
  85. package/src/components/progress/resolveProgressFraction.ts +17 -0
  86. package/src/components/progress/types.ts +27 -0
  87. package/src/components/rating/Rating.tsx +38 -0
  88. package/src/components/rating/index.ts +2 -0
  89. package/src/components/rating/resolveRatingSegments.test.ts +60 -0
  90. package/src/components/rating/resolveRatingSegments.ts +34 -0
  91. package/src/components/rating/types.ts +27 -0
  92. package/src/index.ts +10 -0
  93. package/src/patterns/timeline/Timeline.tsx +104 -0
  94. package/src/patterns/timeline/index.ts +2 -0
  95. package/src/patterns/timeline/types.ts +20 -0
  96. package/src/showcaseCoverage.test.ts +5 -0
  97. package/src/theme/themeScopeStructure.test.ts +10 -0
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+
3
+ import { Box, Inline, Stack } from '../../foundation';
4
+ import { resolveBadgeRecipe, resolveIconSize } from '../../internal/recipes';
5
+ import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
+ import { Badge } from '../badge';
8
+ import { Card } from '../card';
9
+ import { Heading } from '../heading';
10
+ import { Icon } from '../icon';
11
+ import { Text } from '../text';
12
+ import type { MetricCardProps } from './types';
13
+
14
+ function MetricCardInner({
15
+ themeId: _themeId,
16
+ mode: _mode,
17
+ testID,
18
+ label,
19
+ value,
20
+ description,
21
+ icon,
22
+ delta,
23
+ deltaTone = 'neutral',
24
+ actions,
25
+ tone = 'default',
26
+ compact = false,
27
+ onPress,
28
+ }: MetricCardProps) {
29
+ const { theme } = useZoraTheme();
30
+ const isInteractive = Boolean(onPress) && !actions;
31
+
32
+ const badgeRecipe = resolveBadgeRecipe({ tone: deltaTone, emphasis: 'soft', size: 's' });
33
+ const iconColor = theme.semantics.content.muted;
34
+
35
+ return (
36
+ <Card
37
+ compact={compact}
38
+ onPress={isInteractive ? onPress : undefined}
39
+ testID={testID}
40
+ tone={tone}
41
+ >
42
+ <Stack gap={compact ? 's' : 'm'}>
43
+ <Inline align="flex-start" gap="m" justify="space-between">
44
+ <Stack flex={1} gap="xs">
45
+ <Inline align="center" gap="xs" wrap="wrap">
46
+ {icon ? (
47
+ <Icon
48
+ color={iconColor}
49
+ name={icon.name}
50
+ provider={icon.provider}
51
+ size={resolveIconSize('s')}
52
+ />
53
+ ) : null}
54
+ <Text tone="muted" variant="caption" weight="semiBold">
55
+ {label}
56
+ </Text>
57
+ {delta != null ? (
58
+ <Badge
59
+ emphasis={badgeRecipe.variant}
60
+ size={badgeRecipe.size}
61
+ tone={badgeRecipe.tone}
62
+ >
63
+ {delta}
64
+ </Badge>
65
+ ) : null}
66
+ </Inline>
67
+
68
+ <Heading level={compact ? 3 : 2}>{value}</Heading>
69
+
70
+ {description ? (
71
+ <Text tone="muted" variant="bodySmall">
72
+ {description}
73
+ </Text>
74
+ ) : null}
75
+ </Stack>
76
+
77
+ {actions ? <Box>{actions}</Box> : null}
78
+ </Inline>
79
+ </Stack>
80
+ </Card>
81
+ );
82
+ }
83
+
84
+ export const MetricCard = withZoraThemeScope(MetricCardInner);
@@ -0,0 +1,2 @@
1
+ export { MetricCard } from './MetricCard';
2
+ export type { MetricCardProps } from './types';
@@ -0,0 +1,18 @@
1
+ import type { ButtonIconSpec } from '@ankhorage/surface';
2
+ import type React from 'react';
3
+
4
+ import type { ZoraCardTone, ZoraTone } from '../../internal/recipes';
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
6
+
7
+ export interface MetricCardProps extends ZoraBaseProps {
8
+ label: React.ReactNode;
9
+ value: React.ReactNode;
10
+ description?: React.ReactNode;
11
+ icon?: ButtonIconSpec;
12
+ delta?: React.ReactNode;
13
+ deltaTone?: ZoraTone;
14
+ actions?: React.ReactNode;
15
+ tone?: ZoraCardTone;
16
+ compact?: boolean;
17
+ onPress?: () => void;
18
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+
3
+ import { Box } from '../../foundation';
4
+ import { useZoraTheme } from '../../theme/useZoraTheme';
5
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
6
+ import { resolveProgressFraction } from './resolveProgressFraction';
7
+ import { type ProgressProps, resolveProgressRole } from './types';
8
+
9
+ function resolveProgressHeight(size: NonNullable<ProgressProps['size']>): number {
10
+ switch (size) {
11
+ case 's':
12
+ return 4;
13
+ case 'm':
14
+ return 6;
15
+ case 'l':
16
+ default:
17
+ return 8;
18
+ }
19
+ }
20
+
21
+ function ProgressInner({
22
+ themeId: _themeId,
23
+ mode: _mode,
24
+ testID,
25
+ value,
26
+ max = 100,
27
+ tone = 'primary',
28
+ size = 'm',
29
+ }: ProgressProps) {
30
+ const { theme } = useZoraTheme();
31
+ const fraction = resolveProgressFraction({ value, max });
32
+ const height = resolveProgressHeight(size);
33
+ const role = resolveProgressRole(theme, tone);
34
+
35
+ return (
36
+ <Box
37
+ accessibilityRole="progressbar"
38
+ bg={theme.semantics.neutral.surface}
39
+ borderColor={theme.semantics.neutral.divider}
40
+ borderWidth={1}
41
+ radius="full"
42
+ testID={testID}
43
+ style={{ height, overflow: 'hidden' }}
44
+ >
45
+ <Box bg={role.base} style={{ height: '100%', width: `${fraction * 100}%` }} />
46
+ </Box>
47
+ );
48
+ }
49
+
50
+ export const Progress = withZoraThemeScope(ProgressInner);
@@ -0,0 +1,2 @@
1
+ export { Progress } from './Progress';
2
+ export type { ProgressProps } from './types';
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveProgressFraction } from './resolveProgressFraction';
4
+
5
+ describe('resolveProgressFraction', () => {
6
+ test('clamps values to [0, 1]', () => {
7
+ expect(resolveProgressFraction({ value: -5, max: 10 })).toBe(0);
8
+ expect(resolveProgressFraction({ value: 0, max: 10 })).toBe(0);
9
+ expect(resolveProgressFraction({ value: 5, max: 10 })).toBe(0.5);
10
+ expect(resolveProgressFraction({ value: 10, max: 10 })).toBe(1);
11
+ expect(resolveProgressFraction({ value: 15, max: 10 })).toBe(1);
12
+ });
13
+
14
+ test('returns 0 for invalid max', () => {
15
+ expect(resolveProgressFraction({ value: 5, max: 0 })).toBe(0);
16
+ expect(resolveProgressFraction({ value: 5, max: -3 })).toBe(0);
17
+ expect(resolveProgressFraction({ value: 5, max: Number.NaN })).toBe(0);
18
+ });
19
+
20
+ test('returns 0 for invalid value', () => {
21
+ expect(resolveProgressFraction({ value: Number.NaN, max: 10 })).toBe(0);
22
+ });
23
+ });
@@ -0,0 +1,17 @@
1
+ export function resolveProgressFraction({ value, max }: { value: number; max: number }): number {
2
+ if (!Number.isFinite(value) || !Number.isFinite(max) || max <= 0) {
3
+ return 0;
4
+ }
5
+
6
+ const fraction = value / max;
7
+
8
+ if (fraction <= 0) {
9
+ return 0;
10
+ }
11
+
12
+ if (fraction >= 1) {
13
+ return 1;
14
+ }
15
+
16
+ return fraction;
17
+ }
@@ -0,0 +1,27 @@
1
+ import type { RoleSemantics, SurfaceTheme } from '@ankhorage/surface';
2
+
3
+ import type { ZoraControlSize, ZoraTone } from '../../internal/recipes';
4
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
5
+
6
+ export interface ProgressProps extends ZoraBaseProps {
7
+ value: number;
8
+ max?: number;
9
+ tone?: ZoraTone;
10
+ size?: ZoraControlSize;
11
+ }
12
+
13
+ export function resolveProgressRole(theme: SurfaceTheme, tone: ZoraTone): RoleSemantics {
14
+ switch (tone) {
15
+ case 'primary':
16
+ return theme.semantics.action.primary;
17
+ case 'danger':
18
+ return theme.semantics.action.danger;
19
+ case 'success':
20
+ return theme.semantics.success;
21
+ case 'warning':
22
+ return theme.semantics.warning;
23
+ case 'neutral':
24
+ default:
25
+ return theme.semantics.action.neutral;
26
+ }
27
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+
3
+ import { Inline } from '../../foundation';
4
+ import { resolveIconSize } from '../../internal/recipes';
5
+ import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
+ import { Icon } from '../icon';
8
+ import { resolveRatingSegments } from './resolveRatingSegments';
9
+ import { type RatingProps, resolveRatingRole } from './types';
10
+
11
+ function RatingInner({
12
+ themeId: _themeId,
13
+ mode: _mode,
14
+ testID,
15
+ value,
16
+ max = 5,
17
+ tone = 'warning',
18
+ size = 'm',
19
+ }: RatingProps) {
20
+ const { theme } = useZoraTheme();
21
+ const role = resolveRatingRole(theme, tone);
22
+ const segments = resolveRatingSegments({ value, max });
23
+ const iconSize = resolveIconSize(size);
24
+
25
+ return (
26
+ <Inline align="center" gap="xxs" testID={testID} wrap="nowrap">
27
+ {segments.map((segment, index) => {
28
+ const name =
29
+ segment === 'full' ? 'star' : segment === 'half' ? 'star-half' : 'star-outline';
30
+ const color = segment === 'empty' ? theme.semantics.content.muted : role.base;
31
+
32
+ return <Icon key={index} color={color} name={name} size={iconSize} />;
33
+ })}
34
+ </Inline>
35
+ );
36
+ }
37
+
38
+ export const Rating = withZoraThemeScope(RatingInner);
@@ -0,0 +1,2 @@
1
+ export { Rating } from './Rating';
2
+ export type { RatingProps } from './types';
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveRatingSegments } from './resolveRatingSegments';
4
+
5
+ describe('resolveRatingSegments', () => {
6
+ test('returns empty segments for invalid input', () => {
7
+ expect(resolveRatingSegments({ value: 3, max: 0 })).toEqual([]);
8
+ expect(resolveRatingSegments({ value: Number.NaN, max: 5 })).toEqual([]);
9
+ });
10
+
11
+ test('clamps and rounds to nearest half-step', () => {
12
+ expect(resolveRatingSegments({ value: -1, max: 5 })).toEqual([
13
+ 'empty',
14
+ 'empty',
15
+ 'empty',
16
+ 'empty',
17
+ 'empty',
18
+ ]);
19
+
20
+ expect(resolveRatingSegments({ value: 3, max: 5 })).toEqual([
21
+ 'full',
22
+ 'full',
23
+ 'full',
24
+ 'empty',
25
+ 'empty',
26
+ ]);
27
+
28
+ expect(resolveRatingSegments({ value: 3.2, max: 5 })).toEqual([
29
+ 'full',
30
+ 'full',
31
+ 'full',
32
+ 'empty',
33
+ 'empty',
34
+ ]);
35
+
36
+ expect(resolveRatingSegments({ value: 3.25, max: 5 })).toEqual([
37
+ 'full',
38
+ 'full',
39
+ 'full',
40
+ 'half',
41
+ 'empty',
42
+ ]);
43
+
44
+ expect(resolveRatingSegments({ value: 5, max: 5 })).toEqual([
45
+ 'full',
46
+ 'full',
47
+ 'full',
48
+ 'full',
49
+ 'full',
50
+ ]);
51
+
52
+ expect(resolveRatingSegments({ value: 6, max: 5 })).toEqual([
53
+ 'full',
54
+ 'full',
55
+ 'full',
56
+ 'full',
57
+ 'full',
58
+ ]);
59
+ });
60
+ });
@@ -0,0 +1,34 @@
1
+ type RatingSegment = 'full' | 'half' | 'empty';
2
+
3
+ export function resolveRatingSegments({
4
+ value,
5
+ max,
6
+ }: {
7
+ value: number;
8
+ max: number;
9
+ }): RatingSegment[] {
10
+ if (!Number.isFinite(value) || !Number.isFinite(max) || max <= 0) {
11
+ return [];
12
+ }
13
+
14
+ const clamped = Math.max(0, Math.min(value, max));
15
+ const halfSteps = Math.round(clamped * 2);
16
+ const segments: RatingSegment[] = [];
17
+
18
+ for (let index = 0; index < max; index += 1) {
19
+ const threshold = (index + 1) * 2;
20
+ if (halfSteps >= threshold) {
21
+ segments.push('full');
22
+ continue;
23
+ }
24
+
25
+ if (halfSteps === threshold - 1) {
26
+ segments.push('half');
27
+ continue;
28
+ }
29
+
30
+ segments.push('empty');
31
+ }
32
+
33
+ return segments;
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { RoleSemantics, SurfaceTheme } from '@ankhorage/surface';
2
+
3
+ import type { ZoraControlSize, ZoraTone } from '../../internal/recipes';
4
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
5
+
6
+ export interface RatingProps extends ZoraBaseProps {
7
+ value: number;
8
+ max?: number;
9
+ tone?: ZoraTone;
10
+ size?: ZoraControlSize;
11
+ }
12
+
13
+ export function resolveRatingRole(theme: SurfaceTheme, tone: ZoraTone): RoleSemantics {
14
+ switch (tone) {
15
+ case 'primary':
16
+ return theme.semantics.action.primary;
17
+ case 'danger':
18
+ return theme.semantics.action.danger;
19
+ case 'success':
20
+ return theme.semantics.success;
21
+ case 'warning':
22
+ return theme.semantics.warning;
23
+ case 'neutral':
24
+ default:
25
+ return theme.semantics.action.neutral;
26
+ }
27
+ }
package/src/index.ts CHANGED
@@ -60,10 +60,18 @@ export type { IconButtonProps } from './components/icon-button';
60
60
  export { IconButton } from './components/icon-button';
61
61
  export type { InputProps, InputTrailingAction } from './components/input';
62
62
  export { Input } from './components/input';
63
+ export type { MediaCardImageProps, MediaCardProps } from './components/media-card';
64
+ export { MediaCard } from './components/media-card';
65
+ export type { MetricCardProps } from './components/metric-card';
66
+ export { MetricCard } from './components/metric-card';
63
67
  export type { ModalProps } from './components/modal';
64
68
  export { Modal } from './components/modal';
69
+ export type { ProgressProps } from './components/progress';
70
+ export { Progress } from './components/progress';
65
71
  export type { RadioGroupOption, RadioGroupProps, RadioProps } from './components/radio';
66
72
  export { Radio, RadioGroup } from './components/radio';
73
+ export type { RatingProps } from './components/rating';
74
+ export { Rating } from './components/rating';
67
75
  export type { SearchBarProps } from './components/search-bar';
68
76
  export { SearchBar } from './components/search-bar';
69
77
  export type { SelectOption, SelectProps } from './components/select';
@@ -171,6 +179,8 @@ export type { ThemeComposerProps } from './patterns/theme-composer';
171
179
  export { ThemeComposer } from './patterns/theme-composer';
172
180
  export type { PaletteItemProps, TileGridProps } from './patterns/tile-grid';
173
181
  export { PaletteItem, TileGrid } from './patterns/tile-grid';
182
+ export type { TimelineItem, TimelineProps } from './patterns/timeline';
183
+ export { Timeline } from './patterns/timeline';
174
184
  export type { TreeItemNode, TreeItemRenderProps, TreeViewProps } from './patterns/tree-view';
175
185
  export { TreeItem, TreeView } from './patterns/tree-view';
176
186
  export * from './theme';
@@ -0,0 +1,104 @@
1
+ import type { RoleSemantics, SurfaceTheme } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { Heading } from '../../components/heading';
5
+ import { Icon } from '../../components/icon';
6
+ import { Text } from '../../components/text';
7
+ import { Box, Inline, Stack } from '../../foundation';
8
+ import type { ZoraTone } from '../../internal/recipes';
9
+ import { resolveIconSize } from '../../internal/recipes';
10
+ import { useZoraTheme } from '../../theme/useZoraTheme';
11
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
12
+ import type { TimelineItem, TimelineProps } from './types';
13
+
14
+ function resolveRoleSemantics(theme: SurfaceTheme, tone: ZoraTone): RoleSemantics {
15
+ switch (tone) {
16
+ case 'primary':
17
+ return theme.semantics.action.primary;
18
+ case 'danger':
19
+ return theme.semantics.action.danger;
20
+ case 'success':
21
+ return theme.semantics.success;
22
+ case 'warning':
23
+ return theme.semantics.warning;
24
+ case 'neutral':
25
+ default:
26
+ return theme.semantics.action.neutral;
27
+ }
28
+ }
29
+
30
+ function TimelineInner({
31
+ themeId: _themeId,
32
+ mode: _mode,
33
+ testID,
34
+ items,
35
+ compact = false,
36
+ }: TimelineProps) {
37
+ const { theme } = useZoraTheme();
38
+ const gap = compact ? 'm' : 'l';
39
+
40
+ const renderItem = (item: TimelineItem, index: number) => {
41
+ const status = item.status ?? 'neutral';
42
+ const role = resolveRoleSemantics(theme, status);
43
+ const showConnector = index < items.length - 1;
44
+ const iconSize = Math.max(12, resolveIconSize('s'));
45
+
46
+ return (
47
+ <Inline key={item.id} align="flex-start" gap="m" testID={item.testID} wrap="nowrap">
48
+ <Stack align="center" flexShrink={0} gap="xs" style={{ width: 24 }}>
49
+ <Box
50
+ bg={status === 'neutral' ? theme.semantics.neutral.surface : role.softBg}
51
+ borderColor={status === 'neutral' ? theme.semantics.neutral.divider : role.base}
52
+ borderWidth={1}
53
+ height={20}
54
+ radius="full"
55
+ width={20}
56
+ style={{ alignItems: 'center', justifyContent: 'center' }}
57
+ >
58
+ {item.icon ? (
59
+ <Icon
60
+ color={status === 'neutral' ? theme.semantics.content.muted : role.base}
61
+ name={item.icon.name}
62
+ provider={item.icon.provider}
63
+ size={iconSize}
64
+ />
65
+ ) : null}
66
+ </Box>
67
+
68
+ {showConnector ? (
69
+ <Box
70
+ bg={theme.semantics.neutral.divider}
71
+ flex={1}
72
+ radius="full"
73
+ style={{ minHeight: 16, width: 2 }}
74
+ />
75
+ ) : null}
76
+ </Stack>
77
+
78
+ <Stack flex={1} gap="xs" testID={testID}>
79
+ <Inline align="flex-start" gap="s" justify="space-between" wrap="wrap">
80
+ <Heading level={compact ? 4 : 3}>{item.title}</Heading>
81
+ {item.meta ? (
82
+ <Text tone="muted" variant="caption">
83
+ {item.meta}
84
+ </Text>
85
+ ) : null}
86
+ </Inline>
87
+ {item.description ? (
88
+ <Text tone="muted" variant="bodySmall">
89
+ {item.description}
90
+ </Text>
91
+ ) : null}
92
+ </Stack>
93
+ </Inline>
94
+ );
95
+ };
96
+
97
+ return (
98
+ <Stack gap={gap} testID={testID}>
99
+ {items.map(renderItem)}
100
+ </Stack>
101
+ );
102
+ }
103
+
104
+ export const Timeline = withZoraThemeScope(TimelineInner);
@@ -0,0 +1,2 @@
1
+ export { Timeline } from './Timeline';
2
+ export type { TimelineItem, TimelineProps } from './types';
@@ -0,0 +1,20 @@
1
+ import type { ButtonIconSpec } from '@ankhorage/surface';
2
+ import type React from 'react';
3
+
4
+ import type { ZoraTone } from '../../internal/recipes';
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
6
+
7
+ export interface TimelineItem {
8
+ id: string;
9
+ title: React.ReactNode;
10
+ description?: React.ReactNode;
11
+ meta?: React.ReactNode;
12
+ status?: ZoraTone;
13
+ icon?: ButtonIconSpec;
14
+ testID?: string;
15
+ }
16
+
17
+ export interface TimelineProps extends ZoraBaseProps {
18
+ items: readonly TimelineItem[];
19
+ compact?: boolean;
20
+ }
@@ -35,9 +35,13 @@ const REQUIRED_SHOWCASE_COVERAGE = {
35
35
  'Icon',
36
36
  'IconButton',
37
37
  'Input',
38
+ 'MediaCard',
39
+ 'MetricCard',
38
40
  'Modal',
41
+ 'Progress',
39
42
  'Radio',
40
43
  'RadioGroup',
44
+ 'Rating',
41
45
  'SearchBar',
42
46
  'Select',
43
47
  'Tabs',
@@ -88,6 +92,7 @@ const REQUIRED_SHOWCASE_COVERAGE = {
88
92
  'SectionHeader',
89
93
  'SettingsRow',
90
94
  'SwitchField',
95
+ 'Timeline',
91
96
  'ThemeComposer',
92
97
  'PaletteItem',
93
98
  'TileGrid',
@@ -56,6 +56,8 @@ const scopedComponentFiles = [
56
56
  join(srcDir, 'components', 'badge', 'Badge.tsx'),
57
57
  join(srcDir, 'components', 'button', 'Button.tsx'),
58
58
  join(srcDir, 'components', 'card', 'Card.tsx'),
59
+ join(srcDir, 'components', 'media-card', 'MediaCard.tsx'),
60
+ join(srcDir, 'components', 'metric-card', 'MetricCard.tsx'),
59
61
  join(srcDir, 'components', 'checkbox', 'Checkbox.tsx'),
60
62
  join(srcDir, 'components', 'checkbox', 'CheckboxGroup.tsx'),
61
63
  join(srcDir, 'components', 'drawer', 'Drawer.tsx'),
@@ -68,8 +70,10 @@ const scopedComponentFiles = [
68
70
  join(srcDir, 'components', 'icon-button', 'IconButton.tsx'),
69
71
  join(srcDir, 'components', 'input', 'Input.tsx'),
70
72
  join(srcDir, 'components', 'modal', 'Modal.tsx'),
73
+ join(srcDir, 'components', 'progress', 'Progress.tsx'),
71
74
  join(srcDir, 'components', 'radio', 'Radio.tsx'),
72
75
  join(srcDir, 'components', 'radio', 'RadioGroup.tsx'),
76
+ join(srcDir, 'components', 'rating', 'Rating.tsx'),
73
77
  join(srcDir, 'components', 'select', 'Select.tsx'),
74
78
  join(srcDir, 'components', 'tabs', 'Tabs.tsx'),
75
79
  join(srcDir, 'components', 'text', 'Text.tsx'),
@@ -105,6 +109,7 @@ const scopedComponentFiles = [
105
109
  join(srcDir, 'patterns', 'section-header', 'SectionHeader.tsx'),
106
110
  join(srcDir, 'patterns', 'settings-row', 'SettingsRow.tsx'),
107
111
  join(srcDir, 'patterns', 'switch-field', 'SwitchField.tsx'),
112
+ join(srcDir, 'patterns', 'timeline', 'Timeline.tsx'),
108
113
  join(srcDir, 'patterns', 'tile-grid', 'PaletteItem.tsx'),
109
114
  join(srcDir, 'patterns', 'tile-grid', 'TileGrid.tsx'),
110
115
  join(srcDir, 'patterns', 'tree-view', 'TreeItem.tsx'),
@@ -126,6 +131,8 @@ const scopedPropTypeFiles = [
126
131
  join(srcDir, 'components', 'badge', 'types.ts'),
127
132
  join(srcDir, 'components', 'button', 'types.ts'),
128
133
  join(srcDir, 'components', 'card', 'types.ts'),
134
+ join(srcDir, 'components', 'media-card', 'types.ts'),
135
+ join(srcDir, 'components', 'metric-card', 'types.ts'),
129
136
  join(srcDir, 'components', 'checkbox', 'types.ts'),
130
137
  join(srcDir, 'components', 'drawer', 'types.ts'),
131
138
  join(srcDir, 'components', 'form', 'types.ts'),
@@ -134,7 +141,9 @@ const scopedPropTypeFiles = [
134
141
  join(srcDir, 'components', 'icon-button', 'types.ts'),
135
142
  join(srcDir, 'components', 'input', 'types.ts'),
136
143
  join(srcDir, 'components', 'modal', 'types.ts'),
144
+ join(srcDir, 'components', 'progress', 'types.ts'),
137
145
  join(srcDir, 'components', 'radio', 'types.ts'),
146
+ join(srcDir, 'components', 'rating', 'types.ts'),
138
147
  join(srcDir, 'components', 'select', 'types.ts'),
139
148
  join(srcDir, 'components', 'tabs', 'types.ts'),
140
149
  join(srcDir, 'components', 'text', 'types.ts'),
@@ -164,6 +173,7 @@ const scopedPropTypeFiles = [
164
173
  join(srcDir, 'patterns', 'section-header', 'types.ts'),
165
174
  join(srcDir, 'patterns', 'settings-row', 'types.ts'),
166
175
  join(srcDir, 'patterns', 'switch-field', 'types.ts'),
176
+ join(srcDir, 'patterns', 'timeline', 'types.ts'),
167
177
  join(srcDir, 'patterns', 'tile-grid', 'types.ts'),
168
178
  join(srcDir, 'patterns', 'tree-view', 'types.ts'),
169
179
  ] as const;