@ankhorage/zora 1.0.6 → 1.0.7
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/CHANGELOG.md +6 -0
- package/README.md +183 -0
- package/dist/components/media-card/MediaCard.d.ts +4 -0
- package/dist/components/media-card/MediaCard.d.ts.map +1 -0
- package/dist/components/media-card/MediaCard.js +64 -0
- package/dist/components/media-card/MediaCard.js.map +1 -0
- package/dist/components/media-card/index.d.ts +3 -0
- package/dist/components/media-card/index.d.ts.map +1 -0
- package/dist/components/media-card/index.js +2 -0
- package/dist/components/media-card/index.js.map +1 -0
- package/dist/components/media-card/types.d.ts +36 -0
- package/dist/components/media-card/types.d.ts.map +1 -0
- package/dist/components/media-card/types.js +2 -0
- package/dist/components/media-card/types.js.map +1 -0
- package/dist/components/metric-card/MetricCard.d.ts +4 -0
- package/dist/components/metric-card/MetricCard.d.ts.map +1 -0
- package/dist/components/metric-card/MetricCard.js +43 -0
- package/dist/components/metric-card/MetricCard.js.map +1 -0
- package/dist/components/metric-card/index.d.ts +3 -0
- package/dist/components/metric-card/index.d.ts.map +1 -0
- package/dist/components/metric-card/index.js +2 -0
- package/dist/components/metric-card/index.js.map +1 -0
- package/dist/components/metric-card/types.d.ts +17 -0
- package/dist/components/metric-card/types.d.ts.map +1 -0
- package/dist/components/metric-card/types.js +2 -0
- package/dist/components/metric-card/types.js.map +1 -0
- package/dist/components/progress/Progress.d.ts +4 -0
- package/dist/components/progress/Progress.d.ts.map +1 -0
- package/dist/components/progress/Progress.js +28 -0
- package/dist/components/progress/Progress.js.map +1 -0
- package/dist/components/progress/index.d.ts +3 -0
- package/dist/components/progress/index.d.ts.map +1 -0
- package/dist/components/progress/index.js +2 -0
- package/dist/components/progress/index.js.map +1 -0
- package/dist/components/progress/resolveProgressFraction.d.ts +5 -0
- package/dist/components/progress/resolveProgressFraction.d.ts.map +1 -0
- package/dist/components/progress/resolveProgressFraction.js +14 -0
- package/dist/components/progress/resolveProgressFraction.js.map +1 -0
- package/dist/components/progress/types.d.ts +11 -0
- package/dist/components/progress/types.d.ts.map +1 -0
- package/dist/components/progress/types.js +16 -0
- package/dist/components/progress/types.js.map +1 -0
- package/dist/components/rating/Rating.d.ts +4 -0
- package/dist/components/rating/Rating.d.ts.map +1 -0
- package/dist/components/rating/Rating.js +23 -0
- package/dist/components/rating/Rating.js.map +1 -0
- package/dist/components/rating/index.d.ts +3 -0
- package/dist/components/rating/index.d.ts.map +1 -0
- package/dist/components/rating/index.js +2 -0
- package/dist/components/rating/index.js.map +1 -0
- package/dist/components/rating/resolveRatingSegments.d.ts +7 -0
- package/dist/components/rating/resolveRatingSegments.d.ts.map +1 -0
- package/dist/components/rating/resolveRatingSegments.js +22 -0
- package/dist/components/rating/resolveRatingSegments.js.map +1 -0
- package/dist/components/rating/types.d.ts +11 -0
- package/dist/components/rating/types.d.ts.map +1 -0
- package/dist/components/rating/types.js +16 -0
- package/dist/components/rating/types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/patterns/timeline/Timeline.d.ts +4 -0
- package/dist/patterns/timeline/Timeline.d.ts.map +1 -0
- package/dist/patterns/timeline/Timeline.js +59 -0
- package/dist/patterns/timeline/Timeline.js.map +1 -0
- package/dist/patterns/timeline/index.d.ts +3 -0
- package/dist/patterns/timeline/index.d.ts.map +1 -0
- package/dist/patterns/timeline/index.js +2 -0
- package/dist/patterns/timeline/index.js.map +1 -0
- package/dist/patterns/timeline/types.d.ts +18 -0
- package/dist/patterns/timeline/types.d.ts.map +1 -0
- package/dist/patterns/timeline/types.js +2 -0
- package/dist/patterns/timeline/types.js.map +1 -0
- package/package.json +1 -1
- package/src/components/media-card/MediaCard.tsx +120 -0
- package/src/components/media-card/index.ts +2 -0
- package/src/components/media-card/types.ts +44 -0
- package/src/components/metric-card/MetricCard.tsx +84 -0
- package/src/components/metric-card/index.ts +2 -0
- package/src/components/metric-card/types.ts +18 -0
- package/src/components/progress/Progress.tsx +50 -0
- package/src/components/progress/index.ts +2 -0
- package/src/components/progress/resolveProgressFraction.test.ts +23 -0
- package/src/components/progress/resolveProgressFraction.ts +17 -0
- package/src/components/progress/types.ts +27 -0
- package/src/components/rating/Rating.tsx +38 -0
- package/src/components/rating/index.ts +2 -0
- package/src/components/rating/resolveRatingSegments.test.ts +60 -0
- package/src/components/rating/resolveRatingSegments.ts +34 -0
- package/src/components/rating/types.ts +27 -0
- package/src/index.ts +10 -0
- package/src/patterns/timeline/Timeline.tsx +104 -0
- package/src/patterns/timeline/index.ts +2 -0
- package/src/patterns/timeline/types.ts +20 -0
- package/src/showcaseCoverage.test.ts +5 -0
- 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,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,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,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,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;
|