@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
@@ -0,0 +1,212 @@
1
+ import { css, keyframes } from '@emotion/react';
2
+ import styled from '@emotion/styled';
3
+ import Portal from '@os-design/portal';
4
+ import {
5
+ WithSize,
6
+ enableScrollingStyles,
7
+ resetFocusStyles,
8
+ sizeStyles,
9
+ } from '@os-design/styles';
10
+ import { clr, useTheme } from '@os-design/theming';
11
+ import { omitEmotionProps, useBodyScroll, useClosable } from '@os-design/utils';
12
+ import React, { forwardRef, useCallback, useContext } from 'react';
13
+ import FocusLock from 'react-focus-lock';
14
+ import { ModalMask } from '../Modal';
15
+
16
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
17
+ export interface DrawerProps extends JsxDivProps, WithSize {
18
+ /**
19
+ * The placement of the drawer.
20
+ * @default right
21
+ */
22
+ placement?: 'left' | 'right';
23
+ /**
24
+ * The width of the drawer.
25
+ * @default 15em
26
+ */
27
+ width?: string;
28
+ /**
29
+ * Whether the drawer is visible.
30
+ * @default false
31
+ */
32
+ visible?: boolean;
33
+ /**
34
+ * Specifies a callback that will be called when a user clicks the mask.
35
+ * The callback should set the visible state to false.
36
+ * @default undefined
37
+ */
38
+ onClose?: () => void;
39
+ }
40
+
41
+ const DRAWER_CONTAINER_Z_INDEX = 900;
42
+ const DRAWER_BOX_SHADOW_SIZE_EM = 1;
43
+
44
+ const DrawerMask = styled(ModalMask)`
45
+ z-index: ${DRAWER_CONTAINER_Z_INDEX};
46
+ `;
47
+
48
+ const Container = styled.div`
49
+ position: fixed;
50
+ top: 0;
51
+ left: 0;
52
+ right: 0;
53
+ height: 100%;
54
+ z-index: ${DRAWER_CONTAINER_Z_INDEX};
55
+ `;
56
+
57
+ const placementLeftFadeIn = keyframes`
58
+ from { transform: translateX(calc(-100% - ${DRAWER_BOX_SHADOW_SIZE_EM}em)); }
59
+ to { transform: translateX(0); }
60
+ `;
61
+
62
+ const placementLeftFadeOut = keyframes`
63
+ from { transform: translateX(0); }
64
+ to { transform: translateX(calc(-100% - ${DRAWER_BOX_SHADOW_SIZE_EM}em)); }
65
+ `;
66
+
67
+ const placementRightFadeIn = keyframes`
68
+ from { transform: translateX(calc(100vw + ${DRAWER_BOX_SHADOW_SIZE_EM}em)); }
69
+ to { transform: translateX(calc(100vw - 100%)); }
70
+ `;
71
+
72
+ const placementRightFadeOut = keyframes`
73
+ from { transform: translateX(calc(100vw - 100%)); }
74
+ to { transform: translateX(calc(100vw + ${DRAWER_BOX_SHADOW_SIZE_EM}em)); }
75
+ `;
76
+
77
+ const placementLeftVisibleStyles = (p) =>
78
+ p.placement === 'left' &&
79
+ p.visible &&
80
+ css`
81
+ animation: ${placementLeftFadeIn} ${p.theme.transitionDelay}ms forwards;
82
+ `;
83
+
84
+ const placementLeftInvisibleStyles = (p) =>
85
+ p.placement === 'left' &&
86
+ !p.visible &&
87
+ css`
88
+ animation: ${placementLeftFadeOut} ${p.theme.transitionDelay}ms forwards;
89
+ `;
90
+
91
+ const placementRightVisibleStyles = (p) =>
92
+ p.placement === 'right' &&
93
+ p.visible &&
94
+ css`
95
+ animation: ${placementRightFadeIn} ${p.theme.transitionDelay}ms forwards;
96
+ `;
97
+
98
+ const placementRightInvisibleStyles = (p) =>
99
+ p.placement === 'right' &&
100
+ !p.visible &&
101
+ css`
102
+ animation: ${placementRightFadeOut} ${p.theme.transitionDelay}ms forwards;
103
+ `;
104
+
105
+ type ContentProps = Pick<
106
+ DrawerProps,
107
+ 'placement' | 'width' | 'visible' | 'size'
108
+ >;
109
+ const Content = styled(
110
+ 'div',
111
+ omitEmotionProps('placement', 'width', 'visible', 'size')
112
+ )<ContentProps>`
113
+ ${resetFocusStyles};
114
+
115
+ position: absolute;
116
+ top: 0;
117
+ bottom: 0;
118
+ width: ${(p) => p.width};
119
+ padding-bottom: env(safe-area-inset-bottom);
120
+
121
+ background-color: ${(p) => clr(p.theme.colorBg)};
122
+ color: ${(p) => clr(p.theme.colorText)};
123
+ box-shadow: 0 0 ${DRAWER_BOX_SHADOW_SIZE_EM}em
124
+ ${(p) => clr(p.theme.drawerColorBoxShadow)};
125
+
126
+ ${placementLeftVisibleStyles};
127
+ ${placementLeftInvisibleStyles};
128
+ ${placementRightVisibleStyles};
129
+ ${placementRightInvisibleStyles};
130
+
131
+ ${enableScrollingStyles('y')};
132
+ ${sizeStyles};
133
+ `;
134
+
135
+ const CloseDrawerContext = React.createContext<() => void>(() => {});
136
+
137
+ export const useCloseDrawer = () => useContext(CloseDrawerContext);
138
+
139
+ /**
140
+ * The side panel that appears from the edge of the screen.
141
+ */
142
+ const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
143
+ (
144
+ {
145
+ placement = 'right',
146
+ width = '15em',
147
+ visible = false,
148
+ onClose = () => {},
149
+ size,
150
+ children,
151
+ onClick = () => {},
152
+ ...rest
153
+ },
154
+ ref
155
+ ) => {
156
+ const { theme } = useTheme();
157
+ const mounted = useClosable(visible, theme.transitionDelay);
158
+
159
+ useBodyScroll(!visible);
160
+
161
+ const clickHandler = useCallback(
162
+ (e) => {
163
+ e.stopPropagation();
164
+ onClick(e);
165
+ },
166
+ [onClick]
167
+ );
168
+
169
+ const keyDownHandler = useCallback(
170
+ (e) => {
171
+ if (e.key === 'Escape') {
172
+ e.stopPropagation();
173
+ onClose();
174
+ }
175
+ },
176
+ [onClose]
177
+ );
178
+
179
+ if (!mounted) return null;
180
+
181
+ return (
182
+ <Portal>
183
+ <DrawerMask visible={visible} />
184
+ <Container onClick={onClose}>
185
+ <FocusLock autoFocus>
186
+ <CloseDrawerContext.Provider value={onClose}>
187
+ <Content
188
+ placement={placement}
189
+ width={width}
190
+ visible={visible}
191
+ size={size}
192
+ tabIndex={-1}
193
+ onKeyDown={keyDownHandler}
194
+ onClick={clickHandler}
195
+ role='dialog'
196
+ aria-modal
197
+ {...rest}
198
+ ref={ref}
199
+ >
200
+ {children}
201
+ </Content>
202
+ </CloseDrawerContext.Provider>
203
+ </FocusLock>
204
+ </Container>
205
+ </Portal>
206
+ );
207
+ }
208
+ );
209
+
210
+ Drawer.displayName = 'Drawer';
211
+
212
+ export default Drawer;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+
3
+ export interface FormConfigContextProps {
4
+ /**
5
+ * The text that is displayed to the right of the label of the optional field.
6
+ */
7
+ optionalText: string;
8
+ }
9
+
10
+ const FormConfigContext = React.createContext<FormConfigContextProps>({
11
+ optionalText: 'optional',
12
+ });
13
+
14
+ FormConfigContext.displayName = 'FormConfigContext';
15
+
16
+ export default FormConfigContext;
@@ -0,0 +1,49 @@
1
+ import styled from '@emotion/styled';
2
+ import { WithSize, sizeStyles } from '@os-design/styles';
3
+ import { omitEmotionProps } from '@os-design/utils';
4
+ import React, { forwardRef, useMemo } from 'react';
5
+ import FormConfigContext from './FormConfigContext';
6
+
7
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
8
+ export interface FormProps extends JsxDivProps, WithSize {
9
+ /**
10
+ * The text that is displayed to the right of the label of the optional field.
11
+ * @default optional
12
+ */
13
+ optionalText?: string;
14
+ }
15
+
16
+ const StyledForm = styled('div', omitEmotionProps('size'))<WithSize>`
17
+ max-width: 50em;
18
+
19
+ & > * + * {
20
+ margin-top: 1em;
21
+ }
22
+ & > style + * {
23
+ margin-top: 0;
24
+ }
25
+
26
+ ${sizeStyles};
27
+ `;
28
+
29
+ /**
30
+ * The wrapper of the form.
31
+ */
32
+ const Form = forwardRef<HTMLDivElement, FormProps>(
33
+ ({ optionalText = 'optional', ...rest }, ref) => {
34
+ const memoizedOptionalText = useMemo(
35
+ () => ({ optionalText }),
36
+ [optionalText]
37
+ );
38
+
39
+ return (
40
+ <FormConfigContext.Provider value={memoizedOptionalText}>
41
+ <StyledForm {...rest} ref={ref} />
42
+ </FormConfigContext.Provider>
43
+ );
44
+ }
45
+ );
46
+
47
+ Form.displayName = 'Form';
48
+
49
+ export default Form;
@@ -0,0 +1,74 @@
1
+ import styled from '@emotion/styled';
2
+ import { ellipsisStyles, sizeStyles, WithSize } from '@os-design/styles';
3
+ import { clr } from '@os-design/theming';
4
+
5
+ import { omitEmotionProps } from '@os-design/utils';
6
+ import React, { forwardRef } from 'react';
7
+
8
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
9
+ export interface FormDividerProps extends JsxDivProps, WithSize {
10
+ /**
11
+ * The title that is in the center of the divider.
12
+ */
13
+ title: string;
14
+ /**
15
+ * The description that is under the title.
16
+ * @default undefined
17
+ */
18
+ description?: string;
19
+ }
20
+
21
+ const Container = styled('div', omitEmotionProps('size'))<WithSize>`
22
+ display: flex;
23
+ align-items: center;
24
+ margin: 3em 0 1em;
25
+
26
+ &::before,
27
+ &::after {
28
+ content: '';
29
+ flex: 1;
30
+ min-width: 5%;
31
+ max-width: 50%;
32
+ border-top: 1px solid ${(p) => clr(p.theme.formDividerColor)};
33
+ }
34
+
35
+ ${sizeStyles};
36
+ `;
37
+
38
+ const Content = styled.div`
39
+ padding: 0 1.5em;
40
+ text-align: center;
41
+ overflow: hidden;
42
+ `;
43
+
44
+ const Title = styled.div`
45
+ color: ${(p) => clr(p.theme.colorText)};
46
+ font-size: ${(p) => p.theme.sizes.large}em;
47
+ font-weight: 500;
48
+ ${ellipsisStyles};
49
+ `;
50
+
51
+ const Description = styled.div`
52
+ color: ${(p) => clr(p.theme.inputColorPlaceholder)};
53
+ font-size: ${(p) => p.theme.sizes.small}em;
54
+ line-height: 1.2;
55
+ ${ellipsisStyles};
56
+ `;
57
+
58
+ /**
59
+ * The divider line for separating the various elements of the form.
60
+ */
61
+ const FormDivider = forwardRef<HTMLDivElement, FormDividerProps>(
62
+ ({ title, description, ...rest }, ref) => (
63
+ <Container role='separator' {...rest} ref={ref}>
64
+ <Content>
65
+ <Title>{title}</Title>
66
+ <Description>{description}</Description>
67
+ </Content>
68
+ </Container>
69
+ )
70
+ );
71
+
72
+ FormDivider.displayName = 'FormDivider';
73
+
74
+ export default FormDivider;
@@ -0,0 +1,118 @@
1
+ import styled from '@emotion/styled';
2
+ import { sizeStyles, WithSize } from '@os-design/styles';
3
+ import { clr, ThemeOverrider } from '@os-design/theming';
4
+ import { omitEmotionProps } from '@os-design/utils';
5
+ import React, { forwardRef, useContext, useMemo } from 'react';
6
+ import FormConfigContext from '../Form/FormConfigContext';
7
+
8
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
9
+ export interface FormItemProps extends JsxDivProps, WithSize {
10
+ /**
11
+ * Label of the field.
12
+ * @default undefined
13
+ */
14
+ label?: string;
15
+ /**
16
+ * The help message located at the bottom of the field.
17
+ * @default undefined
18
+ */
19
+ help?: string;
20
+ /**
21
+ * The error message located at the bottom of the field.
22
+ * If specified, the field has a red border.
23
+ * @default undefined
24
+ */
25
+ error?: string | null;
26
+ /**
27
+ * Whether the field is optional.
28
+ * @default false
29
+ */
30
+ optional?: boolean;
31
+ }
32
+
33
+ const Container = styled('div', omitEmotionProps('size'))<WithSize>`
34
+ ${sizeStyles};
35
+ `;
36
+
37
+ const Label = styled.label`
38
+ display: inline-flex;
39
+ align-items: center;
40
+ margin-bottom: 0.4em;
41
+ font-size: ${(p) => p.theme.sizes.small}em;
42
+ color: ${(p) => clr(p.theme.colorText)};
43
+ `;
44
+
45
+ const Optional = styled.span`
46
+ color: ${(p) => clr(p.theme.formItemColorOptional)};
47
+ margin-left: 0.3em;
48
+ `;
49
+
50
+ const Help = styled.div`
51
+ margin-top: 0.1em;
52
+ font-size: ${(p) => p.theme.sizes.small}em;
53
+ color: ${(p) => clr(p.theme.formItemColorHelp)};
54
+ `;
55
+
56
+ const Error = styled(Help)`
57
+ color: ${(p) => clr(p.theme.formItemColorError)};
58
+ `;
59
+
60
+ /**
61
+ * The wrapper of the field.
62
+ */
63
+ const FormItem = forwardRef<HTMLDivElement, FormItemProps>(
64
+ ({ label, help, error, optional = false, id, children, ...rest }, ref) => {
65
+ const { optionalText } = useContext(FormConfigContext);
66
+
67
+ const containerId = useMemo(
68
+ () => id || Math.random().toString(36).slice(2, 11),
69
+ [id]
70
+ );
71
+ const fieldId = useMemo(() => `field-${containerId}`, [containerId]);
72
+ const helpId = useMemo(() => `help-${containerId}`, [containerId]);
73
+ const ariaInvalid = useMemo(() => !!error, [error]);
74
+ const ariaDescribedBy = useMemo(
75
+ () => (help ? helpId : undefined),
76
+ [help, helpId]
77
+ );
78
+
79
+ return (
80
+ <Container role='group' id={id} {...rest} ref={ref}>
81
+ {label && (
82
+ <Label htmlFor={fieldId}>
83
+ {label}
84
+ {optional && <Optional>({optionalText})</Optional>}
85
+ </Label>
86
+ )}
87
+
88
+ <div
89
+ id={fieldId}
90
+ aria-invalid={ariaInvalid}
91
+ aria-describedby={ariaDescribedBy}
92
+ >
93
+ <ThemeOverrider
94
+ overrides={(theme) => ({
95
+ inputColorBorder: error
96
+ ? theme.formItemColorError
97
+ : theme.inputColorBorder,
98
+ })}
99
+ >
100
+ {children}
101
+ </ThemeOverrider>
102
+ </div>
103
+
104
+ {error ? (
105
+ <Error aria-live='polite' id={helpId}>
106
+ {error}
107
+ </Error>
108
+ ) : (
109
+ help && <Help id={helpId}>{help}</Help>
110
+ )}
111
+ </Container>
112
+ );
113
+ }
114
+ );
115
+
116
+ FormItem.displayName = 'FormItem';
117
+
118
+ export default FormItem;
@@ -0,0 +1,62 @@
1
+ import styled from '@emotion/styled';
2
+ import { clr } from '@os-design/theming';
3
+ import { omitEmotionProps } from '@os-design/utils';
4
+ import React from 'react';
5
+
6
+ interface StatusProps {
7
+ count: number;
8
+ current: number;
9
+ height: number;
10
+ }
11
+
12
+ interface ContainerProps {
13
+ height: number;
14
+ }
15
+ const Container = styled('div', omitEmotionProps('height'))<ContainerProps>`
16
+ position: absolute;
17
+ bottom: 0;
18
+ left: 0;
19
+ right: 0;
20
+ height: ${(p) => p.height}px;
21
+ `;
22
+
23
+ interface BlockProps {
24
+ count: number;
25
+ index: number;
26
+ current: boolean;
27
+ }
28
+ const Block = styled(
29
+ 'div',
30
+ omitEmotionProps('count', 'index', 'current')
31
+ )<BlockProps>`
32
+ position: absolute;
33
+ left: ${(p) => (100 / p.count) * p.index}%;
34
+ width: ${(p) =>
35
+ p.index < p.count - 1
36
+ ? `calc(${100 / p.count}% - 2px)`
37
+ : `${100 / p.count}%`};
38
+ height: 100%;
39
+
40
+ background-color: ${(p) =>
41
+ p.current
42
+ ? clr(p.theme.galleryStatusColorBgCurrent)
43
+ : clr(p.theme.galleryStatusColorBg)};
44
+ backdrop-filter: blur(0.2em);
45
+ `;
46
+
47
+ const Status: React.FC<StatusProps> = ({ count, current, height }) => (
48
+ <Container height={height}>
49
+ {Array(count)
50
+ .fill(0)
51
+ .map((_, index) => (
52
+ <Block
53
+ key={index} // eslint-disable-line react/no-array-index-key
54
+ count={count}
55
+ index={index}
56
+ current={index === current}
57
+ />
58
+ ))}
59
+ </Container>
60
+ );
61
+
62
+ export default Status;