@ankhorage/zora 1.0.4 → 1.0.6
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 +12 -0
- package/README.md +315 -5
- package/dist/components/avatar/Avatar.d.ts +4 -0
- package/dist/components/avatar/Avatar.d.ts.map +1 -0
- package/dist/components/avatar/Avatar.js +80 -0
- package/dist/components/avatar/Avatar.js.map +1 -0
- package/dist/components/avatar/index.d.ts +4 -0
- package/dist/components/avatar/index.d.ts.map +1 -0
- package/dist/components/avatar/index.js +3 -0
- package/dist/components/avatar/index.js.map +1 -0
- package/dist/components/avatar/resolveAvatarInitials.d.ts +2 -0
- package/dist/components/avatar/resolveAvatarInitials.d.ts.map +1 -0
- package/dist/components/avatar/resolveAvatarInitials.js +44 -0
- package/dist/components/avatar/resolveAvatarInitials.js.map +1 -0
- package/dist/components/avatar/types.d.ts +17 -0
- package/dist/components/avatar/types.d.ts.map +1 -0
- package/dist/components/avatar/types.js +2 -0
- package/dist/components/avatar/types.js.map +1 -0
- package/dist/components/avatar-group/AvatarGroup.d.ts +4 -0
- package/dist/components/avatar-group/AvatarGroup.d.ts.map +1 -0
- package/dist/components/avatar-group/AvatarGroup.js +26 -0
- package/dist/components/avatar-group/AvatarGroup.js.map +1 -0
- package/dist/components/avatar-group/index.d.ts +3 -0
- package/dist/components/avatar-group/index.d.ts.map +1 -0
- package/dist/components/avatar-group/index.js +2 -0
- package/dist/components/avatar-group/index.js.map +1 -0
- package/dist/components/avatar-group/types.d.ts +22 -0
- package/dist/components/avatar-group/types.d.ts.map +1 -0
- package/dist/components/avatar-group/types.js +2 -0
- package/dist/components/avatar-group/types.js.map +1 -0
- package/dist/components/chip/Chip.d.ts +4 -0
- package/dist/components/chip/Chip.d.ts.map +1 -0
- package/dist/components/chip/Chip.js +54 -0
- package/dist/components/chip/Chip.js.map +1 -0
- package/dist/components/chip/index.d.ts +3 -0
- package/dist/components/chip/index.d.ts.map +1 -0
- package/dist/components/chip/index.js +2 -0
- package/dist/components/chip/index.js.map +1 -0
- package/dist/components/chip/resolveChipColors.d.ts +10 -0
- package/dist/components/chip/resolveChipColors.d.ts.map +1 -0
- package/dist/components/chip/resolveChipColors.js +47 -0
- package/dist/components/chip/resolveChipColors.js.map +1 -0
- package/dist/components/chip/types.d.ts +26 -0
- package/dist/components/chip/types.d.ts.map +1 -0
- package/dist/components/chip/types.js +2 -0
- package/dist/components/chip/types.js.map +1 -0
- package/dist/components/chip-group/ChipGroup.d.ts +4 -0
- package/dist/components/chip-group/ChipGroup.d.ts.map +1 -0
- package/dist/components/chip-group/ChipGroup.js +32 -0
- package/dist/components/chip-group/ChipGroup.js.map +1 -0
- package/dist/components/chip-group/index.d.ts +3 -0
- package/dist/components/chip-group/index.d.ts.map +1 -0
- package/dist/components/chip-group/index.js +2 -0
- package/dist/components/chip-group/index.js.map +1 -0
- package/dist/components/chip-group/types.d.ts +31 -0
- package/dist/components/chip-group/types.d.ts.map +1 -0
- package/dist/components/chip-group/types.js +2 -0
- package/dist/components/chip-group/types.js.map +1 -0
- package/dist/components/input/Input.d.ts.map +1 -1
- package/dist/components/input/Input.js +3 -2
- package/dist/components/input/Input.js.map +1 -1
- package/dist/components/input/index.d.ts +1 -1
- package/dist/components/input/index.d.ts.map +1 -1
- package/dist/components/input/index.js.map +1 -1
- package/dist/components/input/types.d.ts +15 -2
- package/dist/components/input/types.d.ts.map +1 -1
- package/dist/components/input/types.js.map +1 -1
- package/dist/components/search-bar/SearchBar.d.ts +4 -0
- package/dist/components/search-bar/SearchBar.d.ts.map +1 -0
- package/dist/components/search-bar/SearchBar.js +18 -0
- package/dist/components/search-bar/SearchBar.js.map +1 -0
- package/dist/components/search-bar/index.d.ts +3 -0
- package/dist/components/search-bar/index.d.ts.map +1 -0
- package/dist/components/search-bar/index.js +2 -0
- package/dist/components/search-bar/index.js.map +1 -0
- package/dist/components/search-bar/types.d.ts +14 -0
- package/dist/components/search-bar/types.d.ts.map +1 -0
- package/dist/components/search-bar/types.js +2 -0
- package/dist/components/search-bar/types.js.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/patterns/filter-bar/FilterBar.d.ts +4 -0
- package/dist/patterns/filter-bar/FilterBar.d.ts.map +1 -0
- package/dist/patterns/filter-bar/FilterBar.js +12 -0
- package/dist/patterns/filter-bar/FilterBar.js.map +1 -0
- package/dist/patterns/filter-bar/index.d.ts +3 -0
- package/dist/patterns/filter-bar/index.d.ts.map +1 -0
- package/dist/patterns/filter-bar/index.js +2 -0
- package/dist/patterns/filter-bar/index.js.map +1 -0
- package/dist/patterns/filter-bar/types.d.ts +9 -0
- package/dist/patterns/filter-bar/types.d.ts.map +1 -0
- package/dist/patterns/filter-bar/types.js +2 -0
- package/dist/patterns/filter-bar/types.js.map +1 -0
- package/dist/patterns/list/List.d.ts +4 -0
- package/dist/patterns/list/List.d.ts.map +1 -0
- package/dist/patterns/list/List.js +35 -0
- package/dist/patterns/list/List.js.map +1 -0
- package/dist/patterns/list/ListRow.d.ts +4 -0
- package/dist/patterns/list/ListRow.d.ts.map +1 -0
- package/dist/patterns/list/ListRow.js +108 -0
- package/dist/patterns/list/ListRow.js.map +1 -0
- package/dist/patterns/list/ListSection.d.ts +4 -0
- package/dist/patterns/list/ListSection.d.ts.map +1 -0
- package/dist/patterns/list/ListSection.js +14 -0
- package/dist/patterns/list/ListSection.js.map +1 -0
- package/dist/patterns/list/index.d.ts +5 -0
- package/dist/patterns/list/index.d.ts.map +1 -0
- package/dist/patterns/list/index.js +4 -0
- package/dist/patterns/list/index.js.map +1 -0
- package/dist/patterns/list/resolveListSeparator.d.ts +5 -0
- package/dist/patterns/list/resolveListSeparator.d.ts.map +1 -0
- package/dist/patterns/list/resolveListSeparator.js +6 -0
- package/dist/patterns/list/resolveListSeparator.js.map +1 -0
- package/dist/patterns/list/types.d.ts +55 -0
- package/dist/patterns/list/types.d.ts.map +1 -0
- package/dist/patterns/list/types.js +2 -0
- package/dist/patterns/list/types.js.map +1 -0
- package/package.json +1 -1
- package/src/components/avatar/Avatar.tsx +133 -0
- package/src/components/avatar/index.ts +3 -0
- package/src/components/avatar/resolveAvatarInitials.test.ts +27 -0
- package/src/components/avatar/resolveAvatarInitials.ts +46 -0
- package/src/components/avatar/types.ts +20 -0
- package/src/components/avatar-group/AvatarGroup.tsx +74 -0
- package/src/components/avatar-group/index.ts +2 -0
- package/src/components/avatar-group/types.ts +24 -0
- package/src/components/chip/Chip.tsx +95 -0
- package/src/components/chip/index.ts +2 -0
- package/src/components/chip/resolveChipColors.ts +65 -0
- package/src/components/chip/types.ts +29 -0
- package/src/components/chip-group/ChipGroup.tsx +66 -0
- package/src/components/chip-group/index.ts +2 -0
- package/src/components/chip-group/types.ts +36 -0
- package/src/components/input/Input.tsx +17 -1
- package/src/components/input/index.ts +1 -1
- package/src/components/input/types.ts +19 -2
- package/src/components/search-bar/SearchBar.tsx +50 -0
- package/src/components/search-bar/index.ts +2 -0
- package/src/components/search-bar/types.ts +14 -0
- package/src/index.ts +22 -1
- package/src/patterns/filter-bar/FilterBar.tsx +25 -0
- package/src/patterns/filter-bar/index.ts +2 -0
- package/src/patterns/filter-bar/types.ts +10 -0
- package/src/patterns/list/List.tsx +72 -0
- package/src/patterns/list/ListRow.tsx +193 -0
- package/src/patterns/list/ListSection.tsx +36 -0
- package/src/patterns/list/index.ts +11 -0
- package/src/patterns/list/resolveListSeparator.test.ts +18 -0
- package/src/patterns/list/resolveListSeparator.ts +8 -0
- package/src/patterns/list/types.ts +67 -0
- package/src/showcaseCoverage.test.ts +9 -0
- package/src/theme/themeScopeStructure.test.ts +4 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { RoleSemantics, SurfaceTheme } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Image } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { Box } from '../../foundation';
|
|
6
|
+
import type { ZoraTone } from '../../internal/recipes';
|
|
7
|
+
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
8
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
9
|
+
import { Icon } from '../icon';
|
|
10
|
+
import { Text, type TextTone, type TextVariant } from '../text';
|
|
11
|
+
import { resolveAvatarInitials } from './resolveAvatarInitials';
|
|
12
|
+
import type { AvatarProps, AvatarShape, AvatarSize } from './types';
|
|
13
|
+
|
|
14
|
+
const AVATAR_SIZES: Record<AvatarSize, number> = {
|
|
15
|
+
xs: 24,
|
|
16
|
+
s: 32,
|
|
17
|
+
m: 40,
|
|
18
|
+
l: 48,
|
|
19
|
+
xl: 64,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function resolveRoleSemantics(theme: SurfaceTheme, tone: ZoraTone): RoleSemantics {
|
|
23
|
+
switch (tone) {
|
|
24
|
+
case 'primary':
|
|
25
|
+
return theme.semantics.action.primary;
|
|
26
|
+
case 'danger':
|
|
27
|
+
return theme.semantics.action.danger;
|
|
28
|
+
case 'success':
|
|
29
|
+
return theme.semantics.success;
|
|
30
|
+
case 'warning':
|
|
31
|
+
return theme.semantics.warning;
|
|
32
|
+
case 'neutral':
|
|
33
|
+
default:
|
|
34
|
+
return theme.semantics.action.neutral;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveTextTone(tone: ZoraTone): TextTone {
|
|
39
|
+
return tone === 'neutral' ? 'default' : tone;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveTextVariant(size: AvatarSize): TextVariant {
|
|
43
|
+
switch (size) {
|
|
44
|
+
case 'xs':
|
|
45
|
+
case 's':
|
|
46
|
+
return 'caption';
|
|
47
|
+
case 'l':
|
|
48
|
+
return 'bodySmall';
|
|
49
|
+
case 'xl':
|
|
50
|
+
return 'lead';
|
|
51
|
+
case 'm':
|
|
52
|
+
default:
|
|
53
|
+
return 'label';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveRadius(shape: AvatarShape): 'full' | 'l' {
|
|
58
|
+
return shape === 'circle' ? 'full' : 'l';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function AvatarInner({
|
|
62
|
+
themeId: _themeId,
|
|
63
|
+
mode: _mode,
|
|
64
|
+
testID,
|
|
65
|
+
source,
|
|
66
|
+
name,
|
|
67
|
+
initials,
|
|
68
|
+
iconFallback,
|
|
69
|
+
label,
|
|
70
|
+
size = 'm',
|
|
71
|
+
shape = 'circle',
|
|
72
|
+
tone = 'neutral',
|
|
73
|
+
}: AvatarProps) {
|
|
74
|
+
const { theme } = useZoraTheme();
|
|
75
|
+
const resolvedSize = AVATAR_SIZES[size];
|
|
76
|
+
const resolvedInitials = initials ?? resolveAvatarInitials(name);
|
|
77
|
+
const role = resolveRoleSemantics(theme, tone);
|
|
78
|
+
const backgroundColor = tone === 'neutral' ? theme.semantics.neutral.surface : role.softBg;
|
|
79
|
+
const contentColor = tone === 'neutral' ? theme.semantics.content.default : role.base;
|
|
80
|
+
const radius = resolveRadius(shape);
|
|
81
|
+
|
|
82
|
+
const renderFallback = () => {
|
|
83
|
+
if (resolvedInitials) {
|
|
84
|
+
return (
|
|
85
|
+
<Text tone={resolveTextTone(tone)} variant={resolveTextVariant(size)} weight="semiBold">
|
|
86
|
+
{resolvedInitials}
|
|
87
|
+
</Text>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (iconFallback) {
|
|
92
|
+
const iconSize = Math.max(16, Math.round(resolvedSize * 0.48));
|
|
93
|
+
return (
|
|
94
|
+
<Icon
|
|
95
|
+
color={contentColor}
|
|
96
|
+
name={iconFallback.name}
|
|
97
|
+
provider={iconFallback.provider}
|
|
98
|
+
size={iconSize}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Box
|
|
108
|
+
accessibilityLabel={label}
|
|
109
|
+
bg={backgroundColor}
|
|
110
|
+
height={resolvedSize}
|
|
111
|
+
radius={radius}
|
|
112
|
+
testID={testID}
|
|
113
|
+
width={resolvedSize}
|
|
114
|
+
style={{
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
justifyContent: 'center',
|
|
117
|
+
overflow: 'hidden',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{source ? (
|
|
121
|
+
<Image
|
|
122
|
+
accessibilityLabel={label}
|
|
123
|
+
source={source}
|
|
124
|
+
style={{ height: '100%', width: '100%' }}
|
|
125
|
+
/>
|
|
126
|
+
) : (
|
|
127
|
+
renderFallback()
|
|
128
|
+
)}
|
|
129
|
+
</Box>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const Avatar = withZoraThemeScope(AvatarInner);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolveAvatarInitials } from './resolveAvatarInitials';
|
|
4
|
+
|
|
5
|
+
describe('resolveAvatarInitials', () => {
|
|
6
|
+
it('returns null for empty input', () => {
|
|
7
|
+
expect(resolveAvatarInitials(undefined)).toBeNull();
|
|
8
|
+
expect(resolveAvatarInitials('')).toBeNull();
|
|
9
|
+
expect(resolveAvatarInitials(' ')).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns the first two characters for a single word', () => {
|
|
13
|
+
expect(resolveAvatarInitials('Zora')).toBe('ZO');
|
|
14
|
+
expect(resolveAvatarInitials('a')).toBe('A');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns first+last initials for multiple words', () => {
|
|
18
|
+
expect(resolveAvatarInitials('Fabio Gartenmann')).toBe('FG');
|
|
19
|
+
expect(resolveAvatarInitials('Fabio Gartenmann')).toBe('FG');
|
|
20
|
+
expect(resolveAvatarInitials('Fabio Gartenmann Example')).toBe('FE');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('handles non-letter characters', () => {
|
|
24
|
+
expect(resolveAvatarInitials('🧠 Brain')).toBe('🧠B');
|
|
25
|
+
expect(resolveAvatarInitials('Brain 🧠')).toBe('B🧠');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function takeFirstCharacter(value: string): string | null {
|
|
2
|
+
const trimmed = value.trim();
|
|
3
|
+
if (!trimmed) return null;
|
|
4
|
+
|
|
5
|
+
const chars = Array.from(trimmed);
|
|
6
|
+
return chars[0] ? chars[0].toUpperCase() : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function takeFirstTwoCharacters(value: string): string | null {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (!trimmed) return null;
|
|
12
|
+
|
|
13
|
+
const chars = Array.from(trimmed);
|
|
14
|
+
const [first, second] = chars;
|
|
15
|
+
if (!first) return null;
|
|
16
|
+
|
|
17
|
+
return `${first}${second ?? ''}`.toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveAvatarInitials(name: string | undefined): string | null {
|
|
21
|
+
if (!name) return null;
|
|
22
|
+
|
|
23
|
+
const parts = name
|
|
24
|
+
.trim()
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.map((part) => part.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
|
|
29
|
+
if (parts.length === 0) return null;
|
|
30
|
+
|
|
31
|
+
if (parts.length === 1) {
|
|
32
|
+
const [part] = parts;
|
|
33
|
+
return part ? takeFirstTwoCharacters(part) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [firstPart] = parts;
|
|
37
|
+
const [lastPart] = parts.slice(-1);
|
|
38
|
+
const first = firstPart ? takeFirstCharacter(firstPart) : null;
|
|
39
|
+
const last = lastPart ? takeFirstCharacter(lastPart) : null;
|
|
40
|
+
|
|
41
|
+
if (!first && !last) return null;
|
|
42
|
+
if (!last) return first;
|
|
43
|
+
if (!first) return last;
|
|
44
|
+
|
|
45
|
+
return `${first}${last}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ZoraTone } from '../../internal/recipes';
|
|
5
|
+
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
6
|
+
|
|
7
|
+
export type AvatarSize = 'xs' | 's' | 'm' | 'l' | 'xl';
|
|
8
|
+
|
|
9
|
+
export type AvatarShape = 'circle' | 'rounded';
|
|
10
|
+
|
|
11
|
+
export interface AvatarProps extends ZoraBaseProps {
|
|
12
|
+
source?: ImageSourcePropType;
|
|
13
|
+
name?: string;
|
|
14
|
+
initials?: string;
|
|
15
|
+
iconFallback?: ButtonIconSpec;
|
|
16
|
+
label?: string;
|
|
17
|
+
size?: AvatarSize;
|
|
18
|
+
shape?: AvatarShape;
|
|
19
|
+
tone?: ZoraTone;
|
|
20
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Box, Stack } from '../../foundation';
|
|
4
|
+
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
5
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
6
|
+
import { Avatar } from '../avatar';
|
|
7
|
+
import type { AvatarGroupItem, AvatarGroupProps } from './types';
|
|
8
|
+
|
|
9
|
+
function defaultOverflowLabel(overflowCount: number): string {
|
|
10
|
+
return `+${overflowCount}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AvatarGroupInner({
|
|
14
|
+
themeId: _themeId,
|
|
15
|
+
mode: _mode,
|
|
16
|
+
testID,
|
|
17
|
+
items,
|
|
18
|
+
max = 4,
|
|
19
|
+
size = 's',
|
|
20
|
+
shape = 'circle',
|
|
21
|
+
overflowLabel = defaultOverflowLabel,
|
|
22
|
+
}: AvatarGroupProps) {
|
|
23
|
+
const { theme } = useZoraTheme();
|
|
24
|
+
|
|
25
|
+
const visibleItems = items.slice(0, max);
|
|
26
|
+
const overflowCount = Math.max(0, items.length - visibleItems.length);
|
|
27
|
+
const overlap =
|
|
28
|
+
size === 'xs' ? 8 : size === 's' ? 10 : size === 'm' ? 12 : size === 'l' ? 14 : 16;
|
|
29
|
+
const borderColor = theme.semantics.surface.default;
|
|
30
|
+
|
|
31
|
+
const renderItem = (item: AvatarGroupItem, index: number) => (
|
|
32
|
+
<Box
|
|
33
|
+
key={item.id ?? `${index}`}
|
|
34
|
+
ml={index === 0 ? 0 : -overlap}
|
|
35
|
+
radius="full"
|
|
36
|
+
borderWidth={2}
|
|
37
|
+
borderColor={borderColor}
|
|
38
|
+
>
|
|
39
|
+
<Avatar
|
|
40
|
+
iconFallback={item.iconFallback}
|
|
41
|
+
initials={item.initials}
|
|
42
|
+
label={item.label}
|
|
43
|
+
name={item.name}
|
|
44
|
+
shape={shape}
|
|
45
|
+
size={size}
|
|
46
|
+
source={item.source}
|
|
47
|
+
tone={item.tone}
|
|
48
|
+
/>
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Stack align="center" direction="row" testID={testID} wrap="nowrap">
|
|
54
|
+
{visibleItems.map(renderItem)}
|
|
55
|
+
{overflowCount > 0 ? (
|
|
56
|
+
<Box
|
|
57
|
+
ml={visibleItems.length === 0 ? 0 : -overlap}
|
|
58
|
+
radius="full"
|
|
59
|
+
borderWidth={2}
|
|
60
|
+
borderColor={borderColor}
|
|
61
|
+
>
|
|
62
|
+
<Avatar
|
|
63
|
+
initials={overflowLabel(overflowCount)}
|
|
64
|
+
size={size}
|
|
65
|
+
shape={shape}
|
|
66
|
+
tone="neutral"
|
|
67
|
+
/>
|
|
68
|
+
</Box>
|
|
69
|
+
) : null}
|
|
70
|
+
</Stack>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const AvatarGroup = withZoraThemeScope(AvatarGroupInner);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ZoraTone } from '../../internal/recipes';
|
|
5
|
+
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
6
|
+
import type { AvatarShape, AvatarSize } from '../avatar';
|
|
7
|
+
|
|
8
|
+
export interface AvatarGroupItem {
|
|
9
|
+
id?: string;
|
|
10
|
+
source?: ImageSourcePropType;
|
|
11
|
+
name?: string;
|
|
12
|
+
initials?: string;
|
|
13
|
+
iconFallback?: ButtonIconSpec;
|
|
14
|
+
label?: string;
|
|
15
|
+
tone?: ZoraTone;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AvatarGroupProps extends ZoraBaseProps {
|
|
19
|
+
items: readonly AvatarGroupItem[];
|
|
20
|
+
max?: number;
|
|
21
|
+
size?: AvatarSize;
|
|
22
|
+
shape?: AvatarShape;
|
|
23
|
+
overflowLabel?: (overflowCount: number) => string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ButtonBase } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Box, Inline } from '../../foundation';
|
|
5
|
+
import { resolveIconSize } from '../../internal/recipes';
|
|
6
|
+
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
7
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
8
|
+
import { Icon } from '../icon';
|
|
9
|
+
import { Text } from '../text';
|
|
10
|
+
import { resolveChipColors } from './resolveChipColors';
|
|
11
|
+
import type { ChipInteractionState, ChipProps } from './types';
|
|
12
|
+
|
|
13
|
+
function resolveChipPadding(size: NonNullable<ChipProps['size']>): {
|
|
14
|
+
px: 's' | 'm';
|
|
15
|
+
py: 'xxs' | 'xs';
|
|
16
|
+
} {
|
|
17
|
+
switch (size) {
|
|
18
|
+
case 's':
|
|
19
|
+
return { px: 's', py: 'xxs' };
|
|
20
|
+
case 'm':
|
|
21
|
+
return { px: 'm', py: 'xs' };
|
|
22
|
+
case 'l':
|
|
23
|
+
default:
|
|
24
|
+
return { px: 'm', py: 'xs' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ChipInner({
|
|
29
|
+
themeId: _themeId,
|
|
30
|
+
mode: _mode,
|
|
31
|
+
testID,
|
|
32
|
+
children,
|
|
33
|
+
icon,
|
|
34
|
+
selected = false,
|
|
35
|
+
disabled = false,
|
|
36
|
+
tone = 'neutral',
|
|
37
|
+
size = 's',
|
|
38
|
+
onPress,
|
|
39
|
+
}: ChipProps) {
|
|
40
|
+
const { theme } = useZoraTheme();
|
|
41
|
+
const padding = resolveChipPadding(size);
|
|
42
|
+
const iconSize = resolveIconSize(size);
|
|
43
|
+
|
|
44
|
+
const renderContent = (state: ChipInteractionState) => {
|
|
45
|
+
const colors = resolveChipColors({ theme, tone, selected, state });
|
|
46
|
+
const textTone = state.disabled
|
|
47
|
+
? 'muted'
|
|
48
|
+
: selected
|
|
49
|
+
? tone === 'neutral'
|
|
50
|
+
? 'default'
|
|
51
|
+
: tone
|
|
52
|
+
: 'default';
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Box
|
|
56
|
+
bg={colors.backgroundColor}
|
|
57
|
+
borderColor={colors.borderColor}
|
|
58
|
+
borderWidth={1}
|
|
59
|
+
px={padding.px}
|
|
60
|
+
py={padding.py}
|
|
61
|
+
radius="full"
|
|
62
|
+
style={{
|
|
63
|
+
alignSelf: 'flex-start',
|
|
64
|
+
opacity: colors.opacity,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Inline align="center" gap="xs" wrap="nowrap">
|
|
68
|
+
{icon ? (
|
|
69
|
+
<Icon
|
|
70
|
+
color={colors.contentColor}
|
|
71
|
+
name={icon.name}
|
|
72
|
+
provider={icon.provider}
|
|
73
|
+
size={iconSize}
|
|
74
|
+
/>
|
|
75
|
+
) : null}
|
|
76
|
+
<Text tone={textTone} variant="label">
|
|
77
|
+
{children}
|
|
78
|
+
</Text>
|
|
79
|
+
</Inline>
|
|
80
|
+
</Box>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!onPress) {
|
|
85
|
+
return renderContent({ disabled, focused: false, hovered: false, pressed: false });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ButtonBase disabled={disabled} onPress={onPress} radius="full" testID={testID}>
|
|
90
|
+
{(state) => renderContent(state)}
|
|
91
|
+
</ButtonBase>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const Chip = withZoraThemeScope(ChipInner);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RoleSemantics, SurfaceTheme } from '@ankhorage/surface';
|
|
2
|
+
|
|
3
|
+
import type { ZoraTone } from '../../internal/recipes';
|
|
4
|
+
import type { ChipColors, ChipInteractionState } from './types';
|
|
5
|
+
|
|
6
|
+
function resolveChipRole(theme: SurfaceTheme, tone: ZoraTone): RoleSemantics {
|
|
7
|
+
switch (tone) {
|
|
8
|
+
case 'primary':
|
|
9
|
+
return theme.semantics.action.primary;
|
|
10
|
+
case 'danger':
|
|
11
|
+
return theme.semantics.action.danger;
|
|
12
|
+
case 'success':
|
|
13
|
+
return theme.semantics.success;
|
|
14
|
+
case 'warning':
|
|
15
|
+
return theme.semantics.warning;
|
|
16
|
+
case 'neutral':
|
|
17
|
+
default:
|
|
18
|
+
return theme.semantics.action.neutral;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveChipColors({
|
|
23
|
+
theme,
|
|
24
|
+
tone,
|
|
25
|
+
selected,
|
|
26
|
+
state,
|
|
27
|
+
}: {
|
|
28
|
+
theme: SurfaceTheme;
|
|
29
|
+
tone: ZoraTone;
|
|
30
|
+
selected: boolean;
|
|
31
|
+
state: ChipInteractionState;
|
|
32
|
+
}): ChipColors {
|
|
33
|
+
if (state.disabled) {
|
|
34
|
+
return {
|
|
35
|
+
backgroundColor: theme.semantics.neutral.surface,
|
|
36
|
+
borderColor: theme.semantics.neutral.divider,
|
|
37
|
+
contentColor: theme.semantics.content.muted,
|
|
38
|
+
opacity: 0.72,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const role = resolveChipRole(theme, tone);
|
|
43
|
+
|
|
44
|
+
if (selected) {
|
|
45
|
+
return {
|
|
46
|
+
backgroundColor: state.pressed
|
|
47
|
+
? role.softActive
|
|
48
|
+
: state.hovered
|
|
49
|
+
? role.softHover
|
|
50
|
+
: role.softBg,
|
|
51
|
+
borderColor: 'transparent',
|
|
52
|
+
contentColor: role.base,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
backgroundColor: state.pressed
|
|
58
|
+
? theme.semantics.neutral.surfaceActive
|
|
59
|
+
: state.hovered
|
|
60
|
+
? theme.semantics.neutral.surfaceHover
|
|
61
|
+
: theme.semantics.neutral.surface,
|
|
62
|
+
borderColor: theme.semantics.neutral.divider,
|
|
63
|
+
contentColor: theme.semantics.content.default,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { ZoraControlSize, ZoraTone } from '../../internal/recipes';
|
|
5
|
+
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
6
|
+
|
|
7
|
+
export interface ChipProps extends ZoraBaseProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
icon?: ButtonIconSpec;
|
|
10
|
+
selected?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
tone?: ZoraTone;
|
|
13
|
+
size?: ZoraControlSize;
|
|
14
|
+
onPress?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChipColors {
|
|
18
|
+
backgroundColor: string;
|
|
19
|
+
borderColor: string;
|
|
20
|
+
contentColor: string;
|
|
21
|
+
opacity?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChipInteractionState {
|
|
25
|
+
pressed: boolean;
|
|
26
|
+
hovered: boolean;
|
|
27
|
+
focused: boolean;
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Inline } from '../../foundation';
|
|
4
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
5
|
+
import { Chip } from '../chip';
|
|
6
|
+
import type { ChipGroupItem, ChipGroupProps } from './types';
|
|
7
|
+
|
|
8
|
+
function hasValue<TValue extends string>(values: readonly TValue[], value: TValue): boolean {
|
|
9
|
+
return values.includes(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toggleValue<TValue extends string>(values: readonly TValue[], value: TValue): TValue[] {
|
|
13
|
+
return hasValue(values, value) ? values.filter((item) => item !== value) : [...values, value];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ChipGroupInner<TValue extends string = string>({
|
|
17
|
+
themeId: _themeId,
|
|
18
|
+
mode: _mode,
|
|
19
|
+
testID,
|
|
20
|
+
items,
|
|
21
|
+
value,
|
|
22
|
+
onValueChange,
|
|
23
|
+
multiple,
|
|
24
|
+
tone = 'neutral',
|
|
25
|
+
size = 's',
|
|
26
|
+
wrap = true,
|
|
27
|
+
disabled,
|
|
28
|
+
}: ChipGroupProps<TValue>) {
|
|
29
|
+
const renderChip = (item: ChipGroupItem<TValue>) => {
|
|
30
|
+
const itemDisabled = disabled ?? item.disabled ?? false;
|
|
31
|
+
const isSelected = Array.isArray(value) ? hasValue(value, item.value) : value === item.value;
|
|
32
|
+
|
|
33
|
+
const handlePress = () => {
|
|
34
|
+
if (multiple) {
|
|
35
|
+
const next = toggleValue(value, item.value);
|
|
36
|
+
onValueChange(next);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onValueChange(item.value);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Chip
|
|
45
|
+
key={item.value}
|
|
46
|
+
disabled={itemDisabled}
|
|
47
|
+
icon={item.icon}
|
|
48
|
+
onPress={handlePress}
|
|
49
|
+
selected={isSelected}
|
|
50
|
+
size={size}
|
|
51
|
+
testID={item.testID}
|
|
52
|
+
tone={tone}
|
|
53
|
+
>
|
|
54
|
+
{item.label}
|
|
55
|
+
</Chip>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Inline align="center" gap="s" testID={testID} wrap={wrap ? 'wrap' : 'nowrap'}>
|
|
61
|
+
{items.map(renderChip)}
|
|
62
|
+
</Inline>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ChipGroup = withZoraThemeScope(ChipGroupInner);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { ZoraControlSize, ZoraTone } from '../../internal/recipes';
|
|
5
|
+
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
6
|
+
|
|
7
|
+
export interface ChipGroupItem<TValue extends string = string> {
|
|
8
|
+
value: TValue;
|
|
9
|
+
label: React.ReactNode;
|
|
10
|
+
icon?: ButtonIconSpec;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
testID?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ChipGroupBaseProps<TValue extends string> extends ZoraBaseProps {
|
|
16
|
+
items: readonly ChipGroupItem<TValue>[];
|
|
17
|
+
tone?: ZoraTone;
|
|
18
|
+
size?: ZoraControlSize;
|
|
19
|
+
wrap?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChipGroupSingleProps<TValue extends string> {
|
|
24
|
+
multiple?: false;
|
|
25
|
+
value: TValue;
|
|
26
|
+
onValueChange: (value: TValue) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ChipGroupMultipleProps<TValue extends string> {
|
|
30
|
+
multiple: true;
|
|
31
|
+
value: readonly TValue[];
|
|
32
|
+
onValueChange: (value: TValue[]) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ChipGroupProps<TValue extends string = string> = ChipGroupBaseProps<TValue> &
|
|
36
|
+
(ChipGroupSingleProps<TValue> | ChipGroupMultipleProps<TValue>);
|
|
@@ -4,6 +4,7 @@ import React from 'react';
|
|
|
4
4
|
import { resolveIconSize } from '../../internal/recipes';
|
|
5
5
|
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
6
6
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
7
|
+
import { IconButton } from '../icon-button';
|
|
7
8
|
import type { InputProps } from './types';
|
|
8
9
|
|
|
9
10
|
function InputInner({
|
|
@@ -12,6 +13,9 @@ function InputInner({
|
|
|
12
13
|
size = 'l',
|
|
13
14
|
leadingIcon,
|
|
14
15
|
trailingIcon,
|
|
16
|
+
trailingAction,
|
|
17
|
+
disabled,
|
|
18
|
+
readOnly,
|
|
15
19
|
...props
|
|
16
20
|
}: InputProps) {
|
|
17
21
|
const { theme } = useZoraTheme();
|
|
@@ -21,6 +25,7 @@ function InputInner({
|
|
|
21
25
|
return (
|
|
22
26
|
<Surface.TextInput
|
|
23
27
|
{...props}
|
|
28
|
+
disabled={disabled}
|
|
24
29
|
leadingAccessory={
|
|
25
30
|
leadingIcon ? (
|
|
26
31
|
<Surface.Icon
|
|
@@ -31,9 +36,20 @@ function InputInner({
|
|
|
31
36
|
/>
|
|
32
37
|
) : undefined
|
|
33
38
|
}
|
|
39
|
+
readOnly={readOnly}
|
|
34
40
|
size={size}
|
|
35
41
|
trailingAccessory={
|
|
36
|
-
|
|
42
|
+
trailingAction ? (
|
|
43
|
+
<IconButton
|
|
44
|
+
icon={trailingAction.icon}
|
|
45
|
+
label={trailingAction.label}
|
|
46
|
+
disabled={disabled ?? readOnly}
|
|
47
|
+
emphasis="ghost"
|
|
48
|
+
size={size === 'l' ? 'm' : size}
|
|
49
|
+
tone="neutral"
|
|
50
|
+
onPress={trailingAction.onPress}
|
|
51
|
+
/>
|
|
52
|
+
) : trailingIcon ? (
|
|
37
53
|
<Surface.Icon
|
|
38
54
|
color={iconColor}
|
|
39
55
|
name={trailingIcon.name}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Input } from './Input';
|
|
2
|
-
export type { InputProps } from './types';
|
|
2
|
+
export type { InputProps, InputTrailingAction } from './types';
|