@os-design/core 1.0.199 → 1.0.200
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/package.json +21 -13
- package/src/@types/emotion.d.ts +7 -0
- package/src/Alert/index.tsx +112 -0
- package/src/Avatar/index.tsx +173 -0
- package/src/Avatar/utils/nameToInitials.ts +12 -0
- package/src/Avatar/utils/strToHue.ts +13 -0
- package/src/AvatarSkeleton/index.tsx +29 -0
- package/src/Breadcrumb/index.tsx +93 -0
- package/src/BreadcrumbItem/index.tsx +83 -0
- package/src/Button/ButtonContent.tsx +91 -0
- package/src/Button/index.tsx +225 -0
- package/src/Button/utils/useButtonColors.ts +84 -0
- package/src/Checkbox/index.tsx +225 -0
- package/src/CheckboxSkeleton/index.tsx +50 -0
- package/src/DatePicker/DatePickerCalendar.tsx +220 -0
- package/src/DatePicker/index.tsx +568 -0
- package/src/Drawer/index.tsx +212 -0
- package/src/Form/FormConfigContext.ts +16 -0
- package/src/Form/index.tsx +49 -0
- package/src/FormDivider/index.tsx +74 -0
- package/src/FormItem/index.tsx +118 -0
- package/src/Gallery/Status.tsx +62 -0
- package/src/Gallery/index.tsx +290 -0
- package/src/GlobalStyles/index.tsx +17 -0
- package/src/GlobalStyles/resetStyles.ts +17 -0
- package/src/GlobalStyles/typographyStyles.ts +78 -0
- package/src/HeaderSkeleton/index.tsx +64 -0
- package/src/Image/index.tsx +104 -0
- package/src/ImageSkeleton/index.tsx +22 -0
- package/src/Input/index.tsx +330 -0
- package/src/Input/utils/getFocusableElements.ts +8 -0
- package/src/InputNumber/index.tsx +208 -0
- package/src/InputNumber/utils/defaultLocale.ts +9 -0
- package/src/InputPassword/index.tsx +201 -0
- package/src/InputPassword/utils/defaultLocale.ts +11 -0
- package/src/InputSearch/index.tsx +111 -0
- package/src/InputSearch/utils/defaultLocale.ts +9 -0
- package/src/InputSkeleton/index.tsx +28 -0
- package/src/Layout/LayoutContext.ts +21 -0
- package/src/Layout/index.tsx +44 -0
- package/src/Link/index.tsx +129 -0
- package/src/LinkButton/index.tsx +100 -0
- package/src/List/WindowScroller.tsx +53 -0
- package/src/List/index.tsx +255 -0
- package/src/List/utils/bodyPointerEvents.ts +24 -0
- package/src/List/utils/frameTimeout.ts +36 -0
- package/src/List/utils/useRWLoadNext.ts +38 -0
- package/src/ListItem/index.tsx +92 -0
- package/src/ListItemActions/index.tsx +207 -0
- package/src/ListItemLink/index.tsx +63 -0
- package/src/ListSkeleton/index.tsx +115 -0
- package/src/LogoLink/index.tsx +93 -0
- package/src/LogoLink/logo.example.svg +18 -0
- package/src/Menu/index.tsx +128 -0
- package/src/Menu/utils/useFocusWithArrows.ts +50 -0
- package/src/MenuDivider/index.tsx +22 -0
- package/src/MenuGroup/index.tsx +190 -0
- package/src/MenuItem/index.tsx +108 -0
- package/src/Modal/index.tsx +411 -0
- package/src/Modal/utils/defaultLocale.ts +9 -0
- package/src/Navigation/index.tsx +214 -0
- package/src/Navigation/utils/useScrollFlags.ts +39 -0
- package/src/NavigationItem/index.tsx +136 -0
- package/src/PageContent/index.tsx +99 -0
- package/src/PageHeader/index.tsx +246 -0
- package/src/PageHeader/utils/defaultLocale.ts +9 -0
- package/src/PageHeaderInputSearch/index.tsx +145 -0
- package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
- package/src/PageHeaderSkeleton/index.tsx +33 -0
- package/src/ParagraphSkeleton/index.tsx +65 -0
- package/src/Popover/index.tsx +243 -0
- package/src/Popover/utils/usePopoverPosition.ts +216 -0
- package/src/Progress/index.tsx +100 -0
- package/src/RadioGroup/index.tsx +165 -0
- package/src/RadioGroupSkeleton/index.tsx +36 -0
- package/src/Result/index.tsx +109 -0
- package/src/ScrollButton/index.tsx +159 -0
- package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
- package/src/ScrollButton/utils/useVisibility.ts +56 -0
- package/src/Select/index.tsx +970 -0
- package/src/Select/utils/defaultLocale.ts +11 -0
- package/src/Skeleton/index.tsx +52 -0
- package/src/Switch/index.tsx +217 -0
- package/src/SwitchSkeleton/index.tsx +30 -0
- package/src/Tag/index.tsx +75 -0
- package/src/TagLink/index.tsx +53 -0
- package/src/TagList/index.tsx +95 -0
- package/src/TagListSkeleton/index.tsx +38 -0
- package/src/TagSkeleton/index.tsx +40 -0
- package/src/TextArea/index.tsx +231 -0
- package/src/TextAreaSkeleton/index.tsx +20 -0
- package/src/ThemeSwitcher/index.tsx +39 -0
- package/src/TimePicker/index.tsx +142 -0
- package/src/Video/index.tsx +41 -0
- package/src/index.ts +125 -0
- package/src/message/AlertIcon.tsx +50 -0
- package/src/message/Message.tsx +108 -0
- package/src/message/index.tsx +64 -0
- package/src/message/styles.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-design/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.200",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": "git@gitlab.com:os-team/libs/os-design.git",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -14,7 +14,15 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"!**/*.test.ts",
|
|
20
|
+
"!**/*.test.tsx",
|
|
21
|
+
"!**/__tests__",
|
|
22
|
+
"!**/*.stories.tsx",
|
|
23
|
+
"!**/*.stories.mdx",
|
|
24
|
+
"!**/*.example.tsx",
|
|
25
|
+
"!**/*.emotion.d.ts"
|
|
18
26
|
],
|
|
19
27
|
"sideEffects": false,
|
|
20
28
|
"scripts": {
|
|
@@ -29,16 +37,16 @@
|
|
|
29
37
|
"access": "public"
|
|
30
38
|
},
|
|
31
39
|
"dependencies": {
|
|
32
|
-
"@os-design/date-picker-utils": "^1.0.
|
|
33
|
-
"@os-design/icons": "^1.0.
|
|
34
|
-
"@os-design/input-number-utils": "^1.0.
|
|
35
|
-
"@os-design/media": "^1.0.
|
|
36
|
-
"@os-design/menu-utils": "^1.0.
|
|
37
|
-
"@os-design/portal": "^1.0.
|
|
38
|
-
"@os-design/styles": "^1.0.
|
|
39
|
-
"@os-design/theming": "^1.0.
|
|
40
|
-
"@os-design/time-picker-utils": "^1.0.
|
|
41
|
-
"@os-design/utils": "^1.0.
|
|
40
|
+
"@os-design/date-picker-utils": "^1.0.17",
|
|
41
|
+
"@os-design/icons": "^1.0.48",
|
|
42
|
+
"@os-design/input-number-utils": "^1.0.22",
|
|
43
|
+
"@os-design/media": "^1.0.19",
|
|
44
|
+
"@os-design/menu-utils": "^1.0.15",
|
|
45
|
+
"@os-design/portal": "^1.0.11",
|
|
46
|
+
"@os-design/styles": "^1.0.45",
|
|
47
|
+
"@os-design/theming": "^1.0.43",
|
|
48
|
+
"@os-design/time-picker-utils": "^1.0.8",
|
|
49
|
+
"@os-design/utils": "^1.0.62",
|
|
42
50
|
"@os-team/password-score": "^1.0.4",
|
|
43
51
|
"facepaint": "^1.2.1",
|
|
44
52
|
"react-focus-lock": "^2.9.4",
|
|
@@ -58,5 +66,5 @@
|
|
|
58
66
|
"react": ">=18",
|
|
59
67
|
"react-dom": ">=18"
|
|
60
68
|
},
|
|
61
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "3d6b264027712ef81a75379fe3fde3c76c3079af"
|
|
62
70
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
|
|
4
|
+
import { CheckCircle, CloseCircle, InfoCircle } from '@os-design/icons';
|
|
5
|
+
import { WithSize, sizeStyles } from '@os-design/styles';
|
|
6
|
+
import { clr } from '@os-design/theming';
|
|
7
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
8
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
11
|
+
export interface AlertProps extends JsxDivProps, WithSize {
|
|
12
|
+
/**
|
|
13
|
+
* Type of styles.
|
|
14
|
+
*/
|
|
15
|
+
type: 'info' | 'success' | 'error';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const infoContainerStyles = (p) =>
|
|
19
|
+
p.type === 'info' &&
|
|
20
|
+
css`
|
|
21
|
+
background-color: ${clr(p.theme.alertInfoColorBg)};
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const successContainerStyles = (p) =>
|
|
25
|
+
p.type === 'success' &&
|
|
26
|
+
css`
|
|
27
|
+
background-color: ${clr(p.theme.alertSuccessColorBg)};
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const errorContainerStyles = (p) =>
|
|
31
|
+
p.type === 'error' &&
|
|
32
|
+
css`
|
|
33
|
+
background-color: ${clr(p.theme.alertErrorColorBg)};
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
type ContainerProps = Pick<AlertProps, 'type' | 'size'>;
|
|
37
|
+
const Container = styled(
|
|
38
|
+
'div',
|
|
39
|
+
omitEmotionProps('type', 'size')
|
|
40
|
+
)<ContainerProps>`
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: row;
|
|
43
|
+
align-items: center;
|
|
44
|
+
|
|
45
|
+
padding: 1em;
|
|
46
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
47
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
48
|
+
|
|
49
|
+
${infoContainerStyles};
|
|
50
|
+
${successContainerStyles};
|
|
51
|
+
${errorContainerStyles};
|
|
52
|
+
${sizeStyles};
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const infoIconContainerStyles = (p) =>
|
|
56
|
+
p.type === 'info' &&
|
|
57
|
+
css`
|
|
58
|
+
color: ${clr(p.theme.alertInfoColorIcon)};
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const successIconContainerStyles = (p) =>
|
|
62
|
+
p.type === 'success' &&
|
|
63
|
+
css`
|
|
64
|
+
color: ${clr(p.theme.alertSuccessColorIcon)};
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const errorIconContainerStyles = (p) =>
|
|
68
|
+
p.type === 'error' &&
|
|
69
|
+
css`
|
|
70
|
+
color: ${clr(p.theme.alertErrorColorIcon)};
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
type IconContainerProps = Pick<AlertProps, 'type'>;
|
|
74
|
+
const IconContainer = styled('i', omitEmotionProps('type'))<IconContainerProps>`
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
|
|
78
|
+
margin-right: 0.3em;
|
|
79
|
+
font-size: 1.4em;
|
|
80
|
+
|
|
81
|
+
${infoIconContainerStyles};
|
|
82
|
+
${successIconContainerStyles};
|
|
83
|
+
${errorIconContainerStyles};
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const typeIconMap = {
|
|
87
|
+
info: InfoCircle,
|
|
88
|
+
success: CheckCircle,
|
|
89
|
+
error: CloseCircle,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The component for feedback.
|
|
94
|
+
*/
|
|
95
|
+
const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
|
96
|
+
({ type, size, children, ...rest }, ref) => {
|
|
97
|
+
const Icon = useMemo(() => typeIconMap[type], [type]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Container size={size} type={type} role='alert' {...rest} ref={ref}>
|
|
101
|
+
<IconContainer type={type}>
|
|
102
|
+
<Icon />
|
|
103
|
+
</IconContainer>
|
|
104
|
+
<span>{children}</span>
|
|
105
|
+
</Container>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
Alert.displayName = 'Alert';
|
|
111
|
+
|
|
112
|
+
export default Alert;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { User } from '@os-design/icons';
|
|
3
|
+
import { WithSize, sizeStyles } from '@os-design/styles';
|
|
4
|
+
import { Color, clr, useTheme } from '@os-design/theming';
|
|
5
|
+
|
|
6
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
7
|
+
import React, { forwardRef, useCallback, useMemo } from 'react';
|
|
8
|
+
import Image, { ImageProps } from '../Image';
|
|
9
|
+
|
|
10
|
+
import nameToInitials from './utils/nameToInitials';
|
|
11
|
+
import strToHue from './utils/strToHue';
|
|
12
|
+
|
|
13
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
14
|
+
export interface AvatarProps extends JsxDivProps, WithSize {
|
|
15
|
+
/**
|
|
16
|
+
* Used to render the first letter if the image URL is not specified.
|
|
17
|
+
* @default undefined
|
|
18
|
+
*/
|
|
19
|
+
firstName?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Used to render the second letter if the image URL is not specified.
|
|
22
|
+
* @default undefined
|
|
23
|
+
*/
|
|
24
|
+
lastName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The URL of the image.
|
|
27
|
+
* @default undefined
|
|
28
|
+
*/
|
|
29
|
+
image?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The props of the image.
|
|
32
|
+
* @default undefined
|
|
33
|
+
*/
|
|
34
|
+
imageProps?: Omit<ImageProps, 'url'>;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the user is online.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
online?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Container = styled('div', omitEmotionProps('size'))<WithSize>`
|
|
43
|
+
position: relative;
|
|
44
|
+
width: 1em;
|
|
45
|
+
height: 1em;
|
|
46
|
+
min-width: 1em;
|
|
47
|
+
min-height: 1em;
|
|
48
|
+
${sizeStyles};
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
interface AvatarContainerProps {
|
|
52
|
+
bgColor: Color;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const AvatarContainer = styled(
|
|
56
|
+
'div',
|
|
57
|
+
omitEmotionProps('bgColor')
|
|
58
|
+
)<AvatarContainerProps>`
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
62
|
+
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
line-height: 1;
|
|
65
|
+
|
|
66
|
+
color: hsl(0, 0%, 100%);
|
|
67
|
+
background-color: ${(p) => clr(p.bgColor)};
|
|
68
|
+
|
|
69
|
+
display: flex;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
align-items: center;
|
|
72
|
+
|
|
73
|
+
white-space: nowrap; // To disable multiline text
|
|
74
|
+
overflow: hidden; // To trim the image corners
|
|
75
|
+
|
|
76
|
+
${sizeStyles};
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const UserIcon = styled(User)`
|
|
80
|
+
font-size: 0.6em;
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const Initials = styled.div`
|
|
84
|
+
font-size: 0.35em;
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const INNER_SIZE = 0.25;
|
|
88
|
+
const OUTER_SIZE = INNER_SIZE * 1.2;
|
|
89
|
+
|
|
90
|
+
const Online = styled.span`
|
|
91
|
+
position: absolute;
|
|
92
|
+
right: -0.05em;
|
|
93
|
+
bottom: -0.05em;
|
|
94
|
+
width: ${OUTER_SIZE}em;
|
|
95
|
+
height: ${OUTER_SIZE}em;
|
|
96
|
+
border-radius: 50%;
|
|
97
|
+
background-color: ${(p) => clr(p.theme.avatarOnlineColorScoop)};
|
|
98
|
+
|
|
99
|
+
&::before {
|
|
100
|
+
content: '';
|
|
101
|
+
position: absolute;
|
|
102
|
+
left: ${(OUTER_SIZE - INNER_SIZE) / 2}em;
|
|
103
|
+
bottom: ${(OUTER_SIZE - INNER_SIZE) / 2}em;
|
|
104
|
+
width: ${INNER_SIZE}em;
|
|
105
|
+
height: ${INNER_SIZE}em;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
background-color: ${(p) => clr(p.theme.avatarOnlineColorBg)};
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Avatar can be used to represent people.
|
|
113
|
+
*/
|
|
114
|
+
const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
|
|
115
|
+
(
|
|
116
|
+
{
|
|
117
|
+
firstName,
|
|
118
|
+
lastName,
|
|
119
|
+
image,
|
|
120
|
+
imageProps = {},
|
|
121
|
+
online = false,
|
|
122
|
+
size = '2em',
|
|
123
|
+
...rest
|
|
124
|
+
},
|
|
125
|
+
ref
|
|
126
|
+
) => {
|
|
127
|
+
const { theme } = useTheme();
|
|
128
|
+
|
|
129
|
+
const bgColor = useMemo<Color>(() => {
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
if (firstName) parts.push(firstName);
|
|
132
|
+
if (lastName) parts.push(lastName);
|
|
133
|
+
if (parts.length > 0) return [strToHue(parts.join(' ')), 30, 60];
|
|
134
|
+
return theme.avatarDefaultColorBg;
|
|
135
|
+
}, [firstName, lastName, theme.avatarDefaultColorBg]);
|
|
136
|
+
|
|
137
|
+
const fullName = useMemo(
|
|
138
|
+
() => [firstName, lastName].filter((i) => i).join(' ') || undefined,
|
|
139
|
+
[firstName, lastName]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const renderChildren = useCallback(() => {
|
|
143
|
+
// Render the image if the image property was specified
|
|
144
|
+
if (image) return <Image url={image} cropped {...imageProps} />;
|
|
145
|
+
|
|
146
|
+
// Render the initials of the name if either firstName or lastName was specified
|
|
147
|
+
const initials = nameToInitials({ firstName, lastName });
|
|
148
|
+
if (initials) return <Initials>{initials}</Initials>;
|
|
149
|
+
|
|
150
|
+
// Otherwise render the user icon
|
|
151
|
+
return <UserIcon />;
|
|
152
|
+
}, [image, imageProps, firstName, lastName]);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Container size={size}>
|
|
156
|
+
<AvatarContainer
|
|
157
|
+
bgColor={bgColor}
|
|
158
|
+
role='img'
|
|
159
|
+
aria-label={fullName || 'User'}
|
|
160
|
+
{...rest}
|
|
161
|
+
ref={ref}
|
|
162
|
+
>
|
|
163
|
+
{renderChildren()}
|
|
164
|
+
</AvatarContainer>
|
|
165
|
+
{online && <Online />}
|
|
166
|
+
</Container>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
Avatar.displayName = 'Avatar';
|
|
172
|
+
|
|
173
|
+
export default Avatar;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Name {
|
|
2
|
+
firstName?: string;
|
|
3
|
+
lastName?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const nameToInitials = ({ firstName, lastName }: Name): string => {
|
|
7
|
+
const first = firstName ? firstName.charAt(0) : '';
|
|
8
|
+
const second = lastName ? lastName.charAt(0) : '';
|
|
9
|
+
return `${first}${second}`.toUpperCase();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default nameToInitials;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the hue of the color by string.
|
|
3
|
+
*/
|
|
4
|
+
const strToHue = (str: string): number => {
|
|
5
|
+
let hash = 0;
|
|
6
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
7
|
+
// eslint-disable-next-line no-bitwise
|
|
8
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
9
|
+
}
|
|
10
|
+
return hash % 360;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default strToHue;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
|
|
3
|
+
import { sizeStyles, WithSize } from '@os-design/styles';
|
|
4
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import Skeleton from '../Skeleton';
|
|
8
|
+
|
|
9
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
10
|
+
export type AvatarSkeletonProps = JsxDivProps & WithSize;
|
|
11
|
+
|
|
12
|
+
const Container = styled('div', omitEmotionProps('size'))<WithSize>`
|
|
13
|
+
${sizeStyles};
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provides an avatar placeholder while a user waits for the content to load.
|
|
18
|
+
*/
|
|
19
|
+
const AvatarSkeleton = forwardRef<HTMLDivElement, AvatarSkeletonProps>(
|
|
20
|
+
({ size = '2em', ...rest }, ref) => (
|
|
21
|
+
<Container size={size} {...rest} ref={ref}>
|
|
22
|
+
<Skeleton width='1em' />
|
|
23
|
+
</Container>
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
AvatarSkeleton.displayName = 'AvatarSkeleton';
|
|
28
|
+
|
|
29
|
+
export default AvatarSkeleton;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { enableScrollingStyles, sizeStyles, WithSize } from '@os-design/styles';
|
|
3
|
+
import { clr } from '@os-design/theming';
|
|
4
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
5
|
+
import React, { forwardRef, ReactElement, useMemo } from 'react';
|
|
6
|
+
import BreadcrumbItem from '../BreadcrumbItem';
|
|
7
|
+
|
|
8
|
+
type JsxOlProps = Omit<JSX.IntrinsicElements['ol'], 'ref'>;
|
|
9
|
+
export interface BreadcrumbProps extends JsxOlProps, WithSize {
|
|
10
|
+
'aria-label'?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Container = styled('ol', omitEmotionProps('size'))<WithSize>`
|
|
14
|
+
list-style: none;
|
|
15
|
+
margin: 0;
|
|
16
|
+
padding: 0;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
20
|
+
|
|
21
|
+
${enableScrollingStyles('x', false)};
|
|
22
|
+
${sizeStyles};
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
let childIndex = 0;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Displays the current location within the hierarchical structure.
|
|
29
|
+
* Implements the Schema.org markup for breadcrumbs.
|
|
30
|
+
* See https://schema.org/BreadcrumbList
|
|
31
|
+
*/
|
|
32
|
+
const Breadcrumb = forwardRef<HTMLOListElement, BreadcrumbProps>(
|
|
33
|
+
({ 'aria-label': ariaLabel = 'Breadcrumb', children, ...rest }, ref) => {
|
|
34
|
+
const breadcrumbItems = useMemo(() => {
|
|
35
|
+
const items: ReactElement[] = [];
|
|
36
|
+
const childrenArray = React.Children.toArray(children);
|
|
37
|
+
childrenArray.forEach((child, index) => {
|
|
38
|
+
if (!React.isValidElement(child)) return;
|
|
39
|
+
|
|
40
|
+
let element;
|
|
41
|
+
const breadcrumbItemProps = {
|
|
42
|
+
key: childIndex,
|
|
43
|
+
position: index + 1,
|
|
44
|
+
hasRightArrow: index < childrenArray.length - 1,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (child.type === BreadcrumbItem) {
|
|
48
|
+
element = React.cloneElement(child, breadcrumbItemProps);
|
|
49
|
+
} else if (
|
|
50
|
+
React.isValidElement(child.props.children) &&
|
|
51
|
+
child.props.children.type === BreadcrumbItem
|
|
52
|
+
) {
|
|
53
|
+
const breadcrumbItem = React.cloneElement(
|
|
54
|
+
child.props.children,
|
|
55
|
+
breadcrumbItemProps
|
|
56
|
+
);
|
|
57
|
+
element = React.cloneElement(
|
|
58
|
+
child,
|
|
59
|
+
{
|
|
60
|
+
...(child.props || {}),
|
|
61
|
+
key: childIndex,
|
|
62
|
+
},
|
|
63
|
+
breadcrumbItem
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!element) return;
|
|
67
|
+
|
|
68
|
+
items.push(element);
|
|
69
|
+
childIndex += 1;
|
|
70
|
+
});
|
|
71
|
+
return items;
|
|
72
|
+
}, [children]);
|
|
73
|
+
|
|
74
|
+
if (breadcrumbItems.length === 0) return null;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<nav aria-label={ariaLabel}>
|
|
78
|
+
<Container
|
|
79
|
+
itemScope
|
|
80
|
+
itemType='https://schema.org/BreadcrumbList'
|
|
81
|
+
{...rest}
|
|
82
|
+
ref={ref}
|
|
83
|
+
>
|
|
84
|
+
{breadcrumbItems}
|
|
85
|
+
</Container>
|
|
86
|
+
</nav>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
Breadcrumb.displayName = 'Breadcrumb';
|
|
92
|
+
|
|
93
|
+
export default Breadcrumb;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { Right } from '@os-design/icons';
|
|
3
|
+
import { ellipsisStyles } from '@os-design/styles';
|
|
4
|
+
import { clr } from '@os-design/theming';
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
import Link, { LinkProps } from '../Link';
|
|
7
|
+
|
|
8
|
+
export interface BreadcrumbItemProps extends LinkProps {
|
|
9
|
+
/**
|
|
10
|
+
* Whether the item is the current page.
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
currentPage?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the right arrow located to the right of the text is visible.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
hasRightArrow?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The position of the breadcrumb item.
|
|
21
|
+
* @default undefined
|
|
22
|
+
*/
|
|
23
|
+
position?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Container = styled.li`
|
|
27
|
+
list-style: none;
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const Name = styled.span`
|
|
31
|
+
max-width: 20em;
|
|
32
|
+
${ellipsisStyles};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const RightIcon = styled(Right)`
|
|
36
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
37
|
+
margin: 0 0.6em;
|
|
38
|
+
font-size: 0.6em;
|
|
39
|
+
opacity: 0.8;
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The item of the breadcrumb.
|
|
44
|
+
*/
|
|
45
|
+
const BreadcrumbItem = forwardRef<HTMLAnchorElement, BreadcrumbItemProps>(
|
|
46
|
+
(
|
|
47
|
+
{
|
|
48
|
+
currentPage = false,
|
|
49
|
+
hasRightArrow = false,
|
|
50
|
+
position,
|
|
51
|
+
href,
|
|
52
|
+
children,
|
|
53
|
+
...rest
|
|
54
|
+
},
|
|
55
|
+
ref
|
|
56
|
+
) => (
|
|
57
|
+
<Container>
|
|
58
|
+
<Link
|
|
59
|
+
itemProp='itemListElement'
|
|
60
|
+
itemScope
|
|
61
|
+
itemType='https://schema.org/ListItem'
|
|
62
|
+
href={href}
|
|
63
|
+
{...(currentPage
|
|
64
|
+
? {
|
|
65
|
+
underline: 'always',
|
|
66
|
+
'aria-current': 'page',
|
|
67
|
+
}
|
|
68
|
+
: {})}
|
|
69
|
+
{...rest}
|
|
70
|
+
ref={ref}
|
|
71
|
+
>
|
|
72
|
+
<link itemProp='item' href={href} />
|
|
73
|
+
{position && <meta itemProp='position' content={position.toString()} />}
|
|
74
|
+
<Name itemProp='name'>{children}</Name>
|
|
75
|
+
</Link>
|
|
76
|
+
{hasRightArrow && <RightIcon role='presentation' />}
|
|
77
|
+
</Container>
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
BreadcrumbItem.displayName = 'BreadcrumbItem';
|
|
82
|
+
|
|
83
|
+
export default BreadcrumbItem;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { keyframes } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
|
|
4
|
+
import { Loading } from '@os-design/icons';
|
|
5
|
+
import { clr } from '@os-design/theming';
|
|
6
|
+
|
|
7
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { LoadingColors } from './utils/useButtonColors';
|
|
11
|
+
|
|
12
|
+
interface ButtonContentProps {
|
|
13
|
+
left?: React.ReactNode;
|
|
14
|
+
right?: React.ReactNode;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
loadingColors: LoadingColors;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LeftAddon = styled.div`
|
|
21
|
+
display: inherit;
|
|
22
|
+
padding-right: ${(p) => p.theme.buttonAddonPaddingHorizontal}em;
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const RightAddon = styled.div`
|
|
26
|
+
display: inherit;
|
|
27
|
+
padding-left: ${(p) => p.theme.buttonAddonPaddingHorizontal}em;
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const Content = styled.span`
|
|
31
|
+
display: inherit;
|
|
32
|
+
|
|
33
|
+
& > svg {
|
|
34
|
+
transform: scale(${(p) => p.theme.buttonIconScaleFactor});
|
|
35
|
+
vertical-align: middle;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const LoadingIcon = styled(Loading)`
|
|
40
|
+
font-size: 1.2em;
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const loadingFadeIn = keyframes`
|
|
44
|
+
from { opacity: 0; }
|
|
45
|
+
to { opacity: 1; }
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
interface LoadingContainerProps {
|
|
49
|
+
colors: LoadingColors;
|
|
50
|
+
}
|
|
51
|
+
const LoadingContainer = styled(
|
|
52
|
+
'div',
|
|
53
|
+
omitEmotionProps('colors')
|
|
54
|
+
)<LoadingContainerProps>`
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 0;
|
|
57
|
+
left: 0;
|
|
58
|
+
right: 0;
|
|
59
|
+
bottom: 0;
|
|
60
|
+
border-radius: inherit;
|
|
61
|
+
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
align-items: center;
|
|
65
|
+
|
|
66
|
+
color: ${(p) => clr(p.colors.text)};
|
|
67
|
+
background-color: ${(p) => clr(p.colors.bg)};
|
|
68
|
+
animation: ${loadingFadeIn} ${(p) => p.theme.transitionDelay}ms;
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
// Used by Button, LinkButton
|
|
72
|
+
const ButtonContent: React.FC<ButtonContentProps> = ({
|
|
73
|
+
left,
|
|
74
|
+
right,
|
|
75
|
+
loading = false,
|
|
76
|
+
loadingColors,
|
|
77
|
+
children,
|
|
78
|
+
}) => (
|
|
79
|
+
<>
|
|
80
|
+
{left && <LeftAddon>{left}</LeftAddon>}
|
|
81
|
+
<Content>{children}</Content>
|
|
82
|
+
{right && <RightAddon>{right}</RightAddon>}
|
|
83
|
+
{loading && (
|
|
84
|
+
<LoadingContainer colors={loadingColors}>
|
|
85
|
+
<LoadingIcon />
|
|
86
|
+
</LoadingContainer>
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export default ButtonContent;
|