@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.
Files changed (99) hide show
  1. package/package.json +21 -13
  2. package/src/@types/emotion.d.ts +7 -0
  3. package/src/Alert/index.tsx +112 -0
  4. package/src/Avatar/index.tsx +173 -0
  5. package/src/Avatar/utils/nameToInitials.ts +12 -0
  6. package/src/Avatar/utils/strToHue.ts +13 -0
  7. package/src/AvatarSkeleton/index.tsx +29 -0
  8. package/src/Breadcrumb/index.tsx +93 -0
  9. package/src/BreadcrumbItem/index.tsx +83 -0
  10. package/src/Button/ButtonContent.tsx +91 -0
  11. package/src/Button/index.tsx +225 -0
  12. package/src/Button/utils/useButtonColors.ts +84 -0
  13. package/src/Checkbox/index.tsx +225 -0
  14. package/src/CheckboxSkeleton/index.tsx +50 -0
  15. package/src/DatePicker/DatePickerCalendar.tsx +220 -0
  16. package/src/DatePicker/index.tsx +568 -0
  17. package/src/Drawer/index.tsx +212 -0
  18. package/src/Form/FormConfigContext.ts +16 -0
  19. package/src/Form/index.tsx +49 -0
  20. package/src/FormDivider/index.tsx +74 -0
  21. package/src/FormItem/index.tsx +118 -0
  22. package/src/Gallery/Status.tsx +62 -0
  23. package/src/Gallery/index.tsx +290 -0
  24. package/src/GlobalStyles/index.tsx +17 -0
  25. package/src/GlobalStyles/resetStyles.ts +17 -0
  26. package/src/GlobalStyles/typographyStyles.ts +78 -0
  27. package/src/HeaderSkeleton/index.tsx +64 -0
  28. package/src/Image/index.tsx +104 -0
  29. package/src/ImageSkeleton/index.tsx +22 -0
  30. package/src/Input/index.tsx +330 -0
  31. package/src/Input/utils/getFocusableElements.ts +8 -0
  32. package/src/InputNumber/index.tsx +208 -0
  33. package/src/InputNumber/utils/defaultLocale.ts +9 -0
  34. package/src/InputPassword/index.tsx +201 -0
  35. package/src/InputPassword/utils/defaultLocale.ts +11 -0
  36. package/src/InputSearch/index.tsx +111 -0
  37. package/src/InputSearch/utils/defaultLocale.ts +9 -0
  38. package/src/InputSkeleton/index.tsx +28 -0
  39. package/src/Layout/LayoutContext.ts +21 -0
  40. package/src/Layout/index.tsx +44 -0
  41. package/src/Link/index.tsx +129 -0
  42. package/src/LinkButton/index.tsx +100 -0
  43. package/src/List/WindowScroller.tsx +53 -0
  44. package/src/List/index.tsx +255 -0
  45. package/src/List/utils/bodyPointerEvents.ts +24 -0
  46. package/src/List/utils/frameTimeout.ts +36 -0
  47. package/src/List/utils/useRWLoadNext.ts +38 -0
  48. package/src/ListItem/index.tsx +92 -0
  49. package/src/ListItemActions/index.tsx +207 -0
  50. package/src/ListItemLink/index.tsx +63 -0
  51. package/src/ListSkeleton/index.tsx +115 -0
  52. package/src/LogoLink/index.tsx +93 -0
  53. package/src/LogoLink/logo.example.svg +18 -0
  54. package/src/Menu/index.tsx +128 -0
  55. package/src/Menu/utils/useFocusWithArrows.ts +50 -0
  56. package/src/MenuDivider/index.tsx +22 -0
  57. package/src/MenuGroup/index.tsx +190 -0
  58. package/src/MenuItem/index.tsx +108 -0
  59. package/src/Modal/index.tsx +411 -0
  60. package/src/Modal/utils/defaultLocale.ts +9 -0
  61. package/src/Navigation/index.tsx +214 -0
  62. package/src/Navigation/utils/useScrollFlags.ts +39 -0
  63. package/src/NavigationItem/index.tsx +136 -0
  64. package/src/PageContent/index.tsx +99 -0
  65. package/src/PageHeader/index.tsx +246 -0
  66. package/src/PageHeader/utils/defaultLocale.ts +9 -0
  67. package/src/PageHeaderInputSearch/index.tsx +145 -0
  68. package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
  69. package/src/PageHeaderSkeleton/index.tsx +33 -0
  70. package/src/ParagraphSkeleton/index.tsx +65 -0
  71. package/src/Popover/index.tsx +243 -0
  72. package/src/Popover/utils/usePopoverPosition.ts +216 -0
  73. package/src/Progress/index.tsx +100 -0
  74. package/src/RadioGroup/index.tsx +165 -0
  75. package/src/RadioGroupSkeleton/index.tsx +36 -0
  76. package/src/Result/index.tsx +109 -0
  77. package/src/ScrollButton/index.tsx +159 -0
  78. package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
  79. package/src/ScrollButton/utils/useVisibility.ts +56 -0
  80. package/src/Select/index.tsx +970 -0
  81. package/src/Select/utils/defaultLocale.ts +11 -0
  82. package/src/Skeleton/index.tsx +52 -0
  83. package/src/Switch/index.tsx +217 -0
  84. package/src/SwitchSkeleton/index.tsx +30 -0
  85. package/src/Tag/index.tsx +75 -0
  86. package/src/TagLink/index.tsx +53 -0
  87. package/src/TagList/index.tsx +95 -0
  88. package/src/TagListSkeleton/index.tsx +38 -0
  89. package/src/TagSkeleton/index.tsx +40 -0
  90. package/src/TextArea/index.tsx +231 -0
  91. package/src/TextAreaSkeleton/index.tsx +20 -0
  92. package/src/ThemeSwitcher/index.tsx +39 -0
  93. package/src/TimePicker/index.tsx +142 -0
  94. package/src/Video/index.tsx +41 -0
  95. package/src/index.ts +125 -0
  96. package/src/message/AlertIcon.tsx +50 -0
  97. package/src/message/Message.tsx +108 -0
  98. package/src/message/index.tsx +64 -0
  99. 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.199",
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.16",
33
- "@os-design/icons": "^1.0.47",
34
- "@os-design/input-number-utils": "^1.0.21",
35
- "@os-design/media": "^1.0.18",
36
- "@os-design/menu-utils": "^1.0.14",
37
- "@os-design/portal": "^1.0.10",
38
- "@os-design/styles": "^1.0.44",
39
- "@os-design/theming": "^1.0.42",
40
- "@os-design/time-picker-utils": "^1.0.7",
41
- "@os-design/utils": "^1.0.61",
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": "bbd193f118a3128033d4d99ffcb4e96fe06f0dba"
69
+ "gitHead": "3d6b264027712ef81a75379fe3fde3c76c3079af"
62
70
  }
@@ -0,0 +1,7 @@
1
+ import '@emotion/react';
2
+ import { Theme as BaseTheme } from '@os-design/theming';
3
+
4
+ declare module '@emotion/react' {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface Theme extends BaseTheme {}
7
+ }
@@ -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;