@charcoal-ui/react-sandbox 1.0.0-alpha.1

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 (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +18 -0
  3. package/package.json +72 -0
  4. package/src/_lib/compat.ts +15 -0
  5. package/src/components/Carousel/index.story.tsx +86 -0
  6. package/src/components/Carousel/index.tsx +382 -0
  7. package/src/components/CarouselButton/index.story.tsx +44 -0
  8. package/src/components/CarouselButton/index.tsx +162 -0
  9. package/src/components/Filter/index.story.tsx +80 -0
  10. package/src/components/Filter/index.tsx +182 -0
  11. package/src/components/HintText/index.story.tsx +19 -0
  12. package/src/components/HintText/index.tsx +95 -0
  13. package/src/components/Layout/index.story.tsx +121 -0
  14. package/src/components/Layout/index.tsx +363 -0
  15. package/src/components/LeftMenu/index.tsx +68 -0
  16. package/src/components/MenuListItem/index.story.tsx +143 -0
  17. package/src/components/MenuListItem/index.tsx +226 -0
  18. package/src/components/Pager/index.story.tsx +102 -0
  19. package/src/components/Pager/index.tsx +255 -0
  20. package/src/components/Spinner/index.story.tsx +47 -0
  21. package/src/components/Spinner/index.tsx +86 -0
  22. package/src/components/SwitchCheckbox/index.story.tsx +32 -0
  23. package/src/components/SwitchCheckbox/index.tsx +147 -0
  24. package/src/components/TextEllipsis/helper.ts +57 -0
  25. package/src/components/TextEllipsis/index.story.tsx +41 -0
  26. package/src/components/TextEllipsis/index.tsx +35 -0
  27. package/src/components/WithIcon/index.story.tsx +145 -0
  28. package/src/components/WithIcon/index.tsx +158 -0
  29. package/src/components/icons/Base.tsx +75 -0
  30. package/src/components/icons/DotsIcon.tsx +33 -0
  31. package/src/components/icons/InfoIcon.tsx +30 -0
  32. package/src/components/icons/NextIcon.tsx +47 -0
  33. package/src/components/icons/WedgeIcon.tsx +57 -0
  34. package/src/foundation/contants.ts +6 -0
  35. package/src/foundation/hooks.ts +195 -0
  36. package/src/foundation/support.ts +29 -0
  37. package/src/foundation/utils.ts +31 -0
  38. package/src/index.ts +45 -0
  39. package/src/misc/storybook-helper.ts +17 -0
  40. package/src/styled.ts +3 -0
  41. package/src/type.d.ts +12 -0
@@ -0,0 +1,363 @@
1
+ import React, { useContext } from 'react'
2
+ import styled, { createGlobalStyle, css } from 'styled-components'
3
+ import {
4
+ MAIN_COLUMN_HORIZONTAL_MIN_MARGIN,
5
+ RESPONSIVE_LEFT_WIDTH,
6
+ RESPONSIVE_MAIN_MAX_WIDTH,
7
+ } from '../../foundation/contants'
8
+ import { useMediaScreen1 } from '../../foundation/hooks'
9
+ import { columnPx, GUTTER_UNIT } from '@charcoal-ui/foundation'
10
+ import { maxWidth } from '@charcoal-ui/utils'
11
+
12
+ interface Props {
13
+ menu?: React.ReactNode
14
+ isHeaderTopMenu?: boolean
15
+ children: React.ReactNode
16
+ header?: React.ReactNode
17
+ wide?: boolean
18
+ center?: boolean
19
+ }
20
+
21
+ const LayoutConfigContext = React.createContext({
22
+ wide: false,
23
+ center: false,
24
+ withLeft: false,
25
+ })
26
+
27
+ export default function Layout({
28
+ menu,
29
+ children,
30
+ header,
31
+ center = false,
32
+ wide,
33
+ isHeaderTopMenu = false,
34
+ }: Props) {
35
+ const config = {
36
+ center,
37
+ wide: center ? true : wide ?? false,
38
+ withLeft: menu != null && !isHeaderTopMenu,
39
+ }
40
+ return (
41
+ <LayoutRoot>
42
+ <LayoutConfigContext.Provider value={config}>
43
+ {config.withLeft && <LeftArea>{menu}</LeftArea>}
44
+ <MainArea center={center}>
45
+ {header != null && <Header>{header}</Header>}
46
+ {isHeaderTopMenu && (
47
+ <HeaderTopMenuContainer>{menu}</HeaderTopMenuContainer>
48
+ )}
49
+ <Grid>{children}</Grid>
50
+ </MainArea>
51
+ </LayoutConfigContext.Provider>
52
+ <GlobalStyle />
53
+ </LayoutRoot>
54
+ )
55
+ }
56
+
57
+ const HeaderTopMenuContainer = styled.div`
58
+ margin-bottom: 40px;
59
+ overflow-x: auto;
60
+ word-break: keep-all;
61
+
62
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
63
+ margin-bottom: 0;
64
+ padding-left: 16px;
65
+ padding-bottom: 16px;
66
+ background-color: ${({ theme }) => theme.color.surface2};
67
+ }
68
+ `
69
+
70
+ const GlobalStyle = createGlobalStyle`
71
+ :root {
72
+ background-color: ${({ theme }) => theme.color.background2};
73
+
74
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
75
+ background-color: ${({ theme }) => theme.color.background1};
76
+ }
77
+ }
78
+ `
79
+
80
+ const LayoutRoot = styled.div`
81
+ display: flex;
82
+ background-color: ${({ theme }) => theme.color.background2};
83
+
84
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
85
+ background-color: ${({ theme }) => theme.color.background1};
86
+ }
87
+ `
88
+
89
+ const LeftArea = styled.div`
90
+ min-width: ${RESPONSIVE_LEFT_WIDTH}px;
91
+ padding: 40px 0 40px ${GUTTER_UNIT}px;
92
+ box-sizing: border-box;
93
+
94
+ @media ${({ theme }) => theme.breakpoint.screen3} {
95
+ display: none;
96
+ }
97
+ `
98
+
99
+ const MainArea = styled.div<{ center: boolean }>`
100
+ flex-grow: 1;
101
+ /* https://www.w3.org/TR/css-flexbox-1/#min-size-auto */
102
+ min-width: 0;
103
+ max-width: ${(p) => (p.center ? columnPx(6) : RESPONSIVE_MAIN_MAX_WIDTH)}px;
104
+ padding: 40px ${MAIN_COLUMN_HORIZONTAL_MIN_MARGIN}px;
105
+ margin: 0 auto;
106
+ display: flex;
107
+ flex-direction: column;
108
+
109
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
110
+ padding: 0;
111
+ }
112
+ `
113
+
114
+ const Header = styled.div`
115
+ font-weight: bold;
116
+ margin-bottom: 12px;
117
+ font-size: 20px;
118
+ line-height: 28px;
119
+ color: ${({ theme }) => theme.color.text2};
120
+
121
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
122
+ margin-bottom: 0;
123
+ padding: 12px;
124
+ font-size: 16px;
125
+ line-height: 24px;
126
+ display: flex;
127
+ justify-content: center;
128
+ background-color: ${({ theme }) => theme.color.surface2};
129
+ }
130
+ `
131
+
132
+ const Grid = styled.div`
133
+ display: grid;
134
+ gap: ${GUTTER_UNIT}px;
135
+ grid-template-columns: 1fr;
136
+ grid-auto-columns: 1fr;
137
+ grid-auto-rows: auto;
138
+
139
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
140
+ gap: 0;
141
+ background-color: ${({ theme }) => theme.color.background1};
142
+ padding-bottom: 60px;
143
+ }
144
+ `
145
+
146
+ interface LayoutItemProps {
147
+ span: number
148
+ children?: React.ReactNode
149
+ }
150
+
151
+ export const LayoutItem = React.forwardRef<HTMLDivElement, LayoutItemProps>(
152
+ function LayoutItem({ span, children }, ref) {
153
+ const { withLeft } = useContext(LayoutConfigContext)
154
+
155
+ return (
156
+ <StyledLayoutItem span={span} withLeft={withLeft} ref={ref}>
157
+ {children}
158
+ </StyledLayoutItem>
159
+ )
160
+ }
161
+ )
162
+
163
+ interface StyledLayoutItemProps {
164
+ span: number
165
+ withLeft: boolean
166
+ }
167
+
168
+ const StyledLayoutItem = styled.div<StyledLayoutItemProps>`
169
+ grid-column-start: auto;
170
+ grid-column-end: span ${(p) => p.span};
171
+ border-radius: 24px;
172
+ color: ${({ theme }) => theme.color.text2};
173
+ background-color: ${({ theme }) => theme.color.background1};
174
+ /* https://www.w3.org/TR/css-grid-1/#min-size-auto */
175
+ min-width: 0;
176
+
177
+ @media ${(p) =>
178
+ p.withLeft ? p.theme.breakpoint.screen4 : p.theme.breakpoint.screen3} {
179
+ ${(p) =>
180
+ p.span > 2 &&
181
+ css`
182
+ grid-column-end: span 2;
183
+ `}
184
+ }
185
+
186
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
187
+ ${(p) =>
188
+ p.span > 1 &&
189
+ css`
190
+ grid-column-end: span 1;
191
+ `}
192
+
193
+ border-radius: 0;
194
+ padding-bottom: 40px;
195
+ }
196
+ `
197
+
198
+ export function LayoutItemHeader({ children }: { children: React.ReactNode }) {
199
+ const { wide, center } = useContext(LayoutConfigContext)
200
+
201
+ return (
202
+ <StyledLayoutItemHeader wide={wide} center={center}>
203
+ {children}
204
+ </StyledLayoutItemHeader>
205
+ )
206
+ }
207
+
208
+ interface StyledLayoutItemHeaderProps {
209
+ wide: boolean
210
+ center: boolean
211
+ }
212
+
213
+ const StyledLayoutItemHeader = styled.div<StyledLayoutItemHeaderProps>`
214
+ padding: 0 ${(p) => (p.wide ? 40 : 24)}px;
215
+ height: ${(p) => (p.wide ? 64 : 48)}px;
216
+ display: grid;
217
+ align-items: center;
218
+ font-size: 16px;
219
+ line-height: 24px;
220
+ font-weight: bold;
221
+ background-color: ${({ theme }) => theme.color.surface2};
222
+ color: ${({ theme }) => theme.color.text2};
223
+ border-radius: 24px 24px 0 0;
224
+ ${(p) =>
225
+ p.center &&
226
+ css`
227
+ justify-content: center;
228
+ `}
229
+
230
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
231
+ margin-top: 4px;
232
+ padding: 0 16px;
233
+ background: none;
234
+ overflow-x: auto;
235
+ border-radius: unset;
236
+ ${(p) =>
237
+ p.wide &&
238
+ css`
239
+ height: 48px;
240
+ margin-top: 0;
241
+ `}
242
+ }
243
+ `
244
+
245
+ export const LAYOUT_ITEM_BODY_PADDING = {
246
+ wide: {
247
+ x: 40,
248
+ y: 40,
249
+ },
250
+ default: {
251
+ x: 24,
252
+ y: 24,
253
+ },
254
+ column1: {
255
+ x: 16,
256
+ y: 16,
257
+ },
258
+ narrow: {
259
+ x: 24,
260
+ yTop: 12,
261
+ yBottom: 20,
262
+ },
263
+ narrowColumn1: {
264
+ x: 16,
265
+ yTop: 4,
266
+ yBottom: 0,
267
+ },
268
+ }
269
+
270
+ export function LayoutItemBody({
271
+ children,
272
+ horizontal = false,
273
+ narrow = false,
274
+ }: {
275
+ children: React.ReactNode
276
+ horizontal?: boolean
277
+ narrow?: boolean
278
+ }) {
279
+ const { wide } = useContext(LayoutConfigContext)
280
+
281
+ return (
282
+ <StyledLayoutItemBody wide={wide} horizontal={horizontal} narrow={narrow}>
283
+ {children}
284
+ </StyledLayoutItemBody>
285
+ )
286
+ }
287
+
288
+ interface StyledLayoutItemBodyProps {
289
+ wide: boolean
290
+ horizontal: boolean
291
+ narrow: boolean
292
+ }
293
+
294
+ export const StyledLayoutItemBody = styled.div<StyledLayoutItemBodyProps>`
295
+ padding: ${(p) =>
296
+ p.narrow
297
+ ? `${LAYOUT_ITEM_BODY_PADDING.narrow.yTop}px ${
298
+ p.horizontal ? 0 : LAYOUT_ITEM_BODY_PADDING.narrow.x
299
+ }px ${LAYOUT_ITEM_BODY_PADDING.narrow.yBottom}px`
300
+ : p.wide
301
+ ? `${p.horizontal ? 0 : LAYOUT_ITEM_BODY_PADDING.wide.y}px ${
302
+ LAYOUT_ITEM_BODY_PADDING.wide.x
303
+ }px`
304
+ : `${p.horizontal ? 0 : LAYOUT_ITEM_BODY_PADDING.default.y}px ${
305
+ LAYOUT_ITEM_BODY_PADDING.default.x
306
+ }px`};
307
+
308
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
309
+ padding: ${(p) =>
310
+ p.narrow
311
+ ? `${LAYOUT_ITEM_BODY_PADDING.narrowColumn1.yTop}px ${
312
+ p.horizontal ? 0 : LAYOUT_ITEM_BODY_PADDING.narrowColumn1.x
313
+ }px ${LAYOUT_ITEM_BODY_PADDING.narrowColumn1.yBottom}px`
314
+ : `${LAYOUT_ITEM_BODY_PADDING.column1.y}px ${
315
+ LAYOUT_ITEM_BODY_PADDING.column1.x
316
+ }px ${0}`};
317
+ }
318
+
319
+ width: 100%;
320
+ box-sizing: border-box;
321
+ `
322
+
323
+ export function useLayoutItemBodyPadding() {
324
+ const { wide } = useContext(LayoutConfigContext)
325
+ return useMediaScreen1()
326
+ ? LAYOUT_ITEM_BODY_PADDING.column1
327
+ : wide
328
+ ? LAYOUT_ITEM_BODY_PADDING.wide
329
+ : LAYOUT_ITEM_BODY_PADDING.default
330
+ }
331
+
332
+ export function CancelLayoutItemBodyPadding({
333
+ children,
334
+ cancelTop,
335
+ }: {
336
+ children: React.ReactNode
337
+ cancelTop?: boolean
338
+ }) {
339
+ const { wide } = useContext(LayoutConfigContext)
340
+
341
+ return (
342
+ <StyledCancelLayoutItemBodyPadding wide={wide} cancelTop={cancelTop}>
343
+ {children}
344
+ </StyledCancelLayoutItemBodyPadding>
345
+ )
346
+ }
347
+
348
+ interface StyledCancelLayoutItemBodyPaddingProps {
349
+ wide: boolean
350
+ cancelTop?: boolean
351
+ }
352
+
353
+ /* eslint-disable max-len */
354
+ export const StyledCancelLayoutItemBodyPadding = styled.div<StyledCancelLayoutItemBodyPaddingProps>`
355
+ margin: 0 -${(p) => (p.wide ? LAYOUT_ITEM_BODY_PADDING.wide.x : LAYOUT_ITEM_BODY_PADDING.default.x)}px;
356
+ margin-top: -${({ cancelTop = false, wide }) => (!cancelTop ? 0 : wide ? LAYOUT_ITEM_BODY_PADDING.wide.y : LAYOUT_ITEM_BODY_PADDING.default.y)}px;
357
+
358
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
359
+ margin: 0 -${LAYOUT_ITEM_BODY_PADDING.column1.x}px;
360
+ margin-top: -${({ cancelTop = false }) => (!cancelTop ? 0 : LAYOUT_ITEM_BODY_PADDING.column1.x)}px;
361
+ }
362
+ `
363
+ /* eslint-enable max-len */
@@ -0,0 +1,68 @@
1
+ import React from 'react'
2
+ import styled from 'styled-components'
3
+ import { MenuListLinkItem } from '../MenuListItem'
4
+ import { useComponentAbstraction } from '@charcoal-ui/react'
5
+
6
+ interface Props<ID extends string> {
7
+ links: readonly {
8
+ text: string
9
+ to: string
10
+ id: ID
11
+ }[]
12
+ active: ID
13
+ }
14
+
15
+ export default function LeftMenu<ID extends string>({
16
+ links,
17
+ active,
18
+ }: Props<ID>) {
19
+ const { Link } = useComponentAbstraction()
20
+ return (
21
+ <Container>
22
+ {links.map((link, index) => (
23
+ <Link to={link.to} key={index}>
24
+ <LinkItem aria-current={link.id === active || undefined}>
25
+ {link.text}
26
+ </LinkItem>
27
+ </Link>
28
+ ))}
29
+ </Container>
30
+ )
31
+ }
32
+
33
+ export function LeftMenuContent<ID extends string>({ links }: Props<ID>) {
34
+ return (
35
+ <>
36
+ {links.map((link, index) => (
37
+ <MenuListLinkItem link={link.to} key={index} primary={link.text} />
38
+ ))}
39
+ </>
40
+ )
41
+ }
42
+
43
+ const Container = styled.div`
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: flex-start;
47
+ `
48
+
49
+ const LinkItem = styled.div`
50
+ display: flex;
51
+ align-items: center;
52
+ color: ${({ theme }) => theme.color.text3};
53
+ border-radius: 24px;
54
+ font-weight: bold;
55
+ font-size: 14px;
56
+ line-height: 22px;
57
+ padding: 0 16px;
58
+ height: 40px;
59
+ transition: 0.2s color;
60
+ &:hover {
61
+ transition: 0.2s color;
62
+ color: ${({ theme }) => theme.color.text2};
63
+ }
64
+ &[aria-current] {
65
+ color: ${({ theme }) => theme.color.text2};
66
+ background-color: ${({ theme }) => theme.color.surface3};
67
+ }
68
+ `
@@ -0,0 +1,143 @@
1
+ import { action } from '@storybook/addon-actions'
2
+ import { boolean, select, text } from '@storybook/addon-knobs'
3
+ import React from 'react'
4
+ import styled from 'styled-components'
5
+ import SwitchCheckbox from '../SwitchCheckbox'
6
+ import WithIcon from '../WithIcon'
7
+ import MenuListItem, {
8
+ MenuListItemContext,
9
+ MenuListLabel,
10
+ MenuListLinkItem,
11
+ MenuListLinkItemWithIcon,
12
+ MenuListSpacer,
13
+ } from '.'
14
+
15
+ export default {
16
+ title: 'Sandbox/MenuListItem',
17
+ component: MenuListItem,
18
+ }
19
+
20
+ export const Default = () => {
21
+ const primary = text('primary', 'Knob to change')
22
+ const secondary = text('secondary', '')
23
+ const disabled = boolean('diasbled', false)
24
+ const padding = select('padding', { '16': 16, '24': 24 }, 24)
25
+ const noHover = boolean('noHover', false)
26
+ return (
27
+ <MenuListItemContext.Provider value={{ padding }}>
28
+ <MenuListItem
29
+ primary={primary}
30
+ secondary={secondary === '' ? undefined : secondary}
31
+ disabled={disabled}
32
+ onClick={action('click')}
33
+ noHover={noHover}
34
+ />
35
+ </MenuListItemContext.Provider>
36
+ )
37
+ }
38
+
39
+ export const Simple = () => (
40
+ <MenuListItem
41
+ primary="Simple item"
42
+ secondary="with secondary"
43
+ onClick={action('click')}
44
+ />
45
+ )
46
+
47
+ export const Disabled = () => (
48
+ <MenuListItem
49
+ primary="Disabled item"
50
+ disabled
51
+ onClick={action('disabled click')}
52
+ />
53
+ )
54
+
55
+ export const Link = () => (
56
+ <MenuListLinkItem
57
+ primary="This is link"
58
+ onClick={action('link click')}
59
+ link="#linkTo"
60
+ />
61
+ )
62
+
63
+ export const HardLink = () => (
64
+ <MenuListLinkItem
65
+ primary='This is link with target "_blank"'
66
+ onClick={action('link click')}
67
+ link="#linkTo"
68
+ target="_blank"
69
+ rel="noopener noreferrer"
70
+ />
71
+ )
72
+
73
+ export const InlineIcon = () => (
74
+ <MenuListItem
75
+ primary={
76
+ <WithIcon icon={<TestInlineIcon />}>Label with inline icon</WithIcon>
77
+ }
78
+ onClick={action('toggle')}
79
+ />
80
+ )
81
+
82
+ export const Icon = () => (
83
+ <MenuListLinkItemWithIcon
84
+ primary="Link with 24px icon"
85
+ icon={<TestIcon />}
86
+ link="#linkTo"
87
+ />
88
+ )
89
+
90
+ export const NoHoverEffect = () => (
91
+ <MenuListItem
92
+ primary="With toggle (no hover effect)"
93
+ onClick={action('toggle')}
94
+ noHover
95
+ >
96
+ <SwitchCheckbox checked onChange={action('toggle')} />
97
+ </MenuListItem>
98
+ )
99
+
100
+ export const Spacer = () => (
101
+ <>
102
+ <MenuListItem primary="↓ This is spacer" />
103
+ <MenuListSpacer />
104
+ <MenuListItem primary="↑ This is spacer" />
105
+ </>
106
+ )
107
+
108
+ export const Label = () => (
109
+ <>
110
+ <MenuListLabel>Label</MenuListLabel>
111
+ <MenuListItem primary="Label grouped items" />
112
+ </>
113
+ )
114
+
115
+ export const TextEllipsis = () => (
116
+ <div
117
+ css={`
118
+ width: 300px;
119
+ `}
120
+ >
121
+ <MenuListItem primary="Loooooooooooooooooooooooooong texxxxxxxxxxxxxxxxxxxxxxxxxt" />
122
+ </div>
123
+ )
124
+
125
+ const TestIcon = styled.div`
126
+ display: inline-block;
127
+ width: 24px;
128
+ height: 24px;
129
+ background-color: currentColor;
130
+ `
131
+
132
+ const TestInlineIcon = styled.div`
133
+ display: inline-flex;
134
+ vertical-align: top;
135
+ align-items: center;
136
+ &::before {
137
+ content: '';
138
+ display: inline-block;
139
+ height: 16px;
140
+ width: 16px;
141
+ background-color: currentColor;
142
+ }
143
+ `