@dbosoft/nextjs-uicore 1.0.0

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 (38) hide show
  1. package/.eslintrc.js +4 -0
  2. package/CHANGELOG.md +12 -0
  3. package/package.json +40 -0
  4. package/src/global.d.ts +1 -0
  5. package/src/head/global.d.ts +1 -0
  6. package/src/head/index.tsx +119 -0
  7. package/src/subnav/helpers/useStuckRef.ts +50 -0
  8. package/src/subnav/index.tsx +133 -0
  9. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -0
  10. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -0
  11. package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +84 -0
  12. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.test.js +25 -0
  13. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -0
  14. package/src/subnav/partials/CtaLinks/icons/github.svg +4 -0
  15. package/src/subnav/partials/CtaLinks/index.tsx +46 -0
  16. package/src/subnav/partials/MenuItemsDefault/index.tsx +63 -0
  17. package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -0
  18. package/src/subnav/partials/MenuItemsOverflow/index.tsx +50 -0
  19. package/src/subnav/partials/MenuItemsOverflow/style.module.scss +51 -0
  20. package/src/subnav/partials/TitleLink/index.tsx +25 -0
  21. package/src/subnav/partials/nav-item-text/index.tsx +29 -0
  22. package/src/subnav/partials/nav-item-text/style.module.scss +19 -0
  23. package/src/subnav/style.module.scss +13 -0
  24. package/src/tabs/hooks/use-scroll-left.ts +27 -0
  25. package/src/tabs/hooks/use-window-size.js +33 -0
  26. package/src/tabs/icons/chevron-right.svg +1 -0
  27. package/src/tabs/icons/tooltip.svg +1 -0
  28. package/src/tabs/index.tsx +102 -0
  29. package/src/tabs/partials/tab-trigger/index.tsx +66 -0
  30. package/src/tabs/partials/tab-trigger/style.module.scss +69 -0
  31. package/src/tabs/partials/tab-triggers/index.tsx +241 -0
  32. package/src/tabs/partials/tab-triggers/style.module.scss +193 -0
  33. package/src/tabs/partials/tooltip/index.tsx +112 -0
  34. package/src/tabs/partials/tooltip/style.module.scss +38 -0
  35. package/src/tabs/provider.js +18 -0
  36. package/src/tabs/style.module.css +4 -0
  37. package/src/tabs/utils/smooth-scroll.js +88 -0
  38. package/tsconfig.json +8 -0
@@ -0,0 +1,71 @@
1
+ .root {
2
+ display: flex;
3
+ padding: 0 32px;
4
+ margin: 0 auto;
5
+ align-items: center;
6
+ list-style: none;
7
+
8
+ &.alignRight {
9
+ margin-right: 0;
10
+ padding-right: 16px;
11
+ }
12
+ }
13
+
14
+ .listItem {
15
+ position: relative;
16
+ white-space: nowrap;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+
21
+ .navLink {
22
+ composes: g-type-body-small-strong from global;
23
+ position: relative;
24
+ padding: 0 16px;
25
+ line-height: 2.5rem;
26
+ display: flex;
27
+ align-items: center;
28
+ white-space: nowrap;
29
+ }
30
+
31
+ .submenuItem {
32
+ composes: g-type-body-small-strong from global;
33
+ color: var(--black);
34
+ }
35
+
36
+ .submenuModal {
37
+ border-radius: 4px;
38
+ box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
39
+ background: var(--white);
40
+ z-index: 1;
41
+ display: block;
42
+ position: absolute;
43
+ top: 100%;
44
+ margin: 8px 0 0 0;
45
+ padding: 24px;
46
+ left: 50%;
47
+ transform: translateX(-50%);
48
+ list-style: none;
49
+
50
+ & li {
51
+ & a:hover .text {
52
+ text-decoration: underline;
53
+ }
54
+
55
+ & + li {
56
+ margin-top: 6px;
57
+ }
58
+ }
59
+
60
+ &.isCollapsed {
61
+ display: none;
62
+ }
63
+ }
64
+
65
+ .verticalDivider {
66
+ background: var(--gray-5);
67
+ height: 1.75rem;
68
+ width: 1px;
69
+ margin: 0 8px;
70
+ display: block;
71
+ }
@@ -0,0 +1,50 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react'
3
+ import LinkWrap from '@dbosoft/react-uicore/link-wrap'
4
+ import CtaLinks from '../CtaLinks'
5
+ import type { ICtaItem, MenuItem } from '../..'
6
+ import s from './style.module.scss'
7
+ import Link from 'next/link';
8
+
9
+
10
+ interface MenuItemsOverflowProps {
11
+ menuItems: MenuItem[],
12
+ ctaLinks: ICtaItem[],
13
+ hideGithubStars: boolean,
14
+ }
15
+
16
+
17
+ export default function MenuItemsOverflow({ menuItems, ctaLinks, hideGithubStars }: MenuItemsOverflowProps) {
18
+ return (<><ul className={s.ulElem}>
19
+ {menuItems.map((menuItem, stableIdx) => {
20
+ if (menuItem === 'divider') return null
21
+
22
+ const { text, url } = menuItem
23
+ return (
24
+ <SubmenuItem
25
+ active={menuItem.active || false}
26
+ key={stableIdx}
27
+ text={text}
28
+ url={url}
29
+ />
30
+ )
31
+ })}
32
+ </ul>
33
+ <CtaLinks
34
+ hideGithubStars={hideGithubStars}
35
+ isInDropdown
36
+ links={ctaLinks}
37
+ /></>)
38
+
39
+ }
40
+
41
+ function SubmenuItem({ url, text, active }: { url: string; text: string; active: boolean }) {
42
+ return (
43
+ <li>
44
+ <LinkWrap Link={Link} className={s.submenuItem} href={url} title={text}>
45
+ {text}
46
+ </LinkWrap>
47
+ </li>
48
+ )
49
+ }
50
+
@@ -0,0 +1,51 @@
1
+ .root {
2
+ position: relative;
3
+ margin-left: auto;
4
+ }
5
+
6
+ .dropdown {
7
+ background: var(--white);
8
+ border-radius: 4px;
9
+ box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
10
+ margin-top: 8px;
11
+ max-height: calc(100vh - 300%);
12
+ overflow-y: auto;
13
+ padding: 12px 24px 0 24px;
14
+ position: absolute;
15
+ right: 0;
16
+ top: 100%;
17
+ width: 256px;
18
+ z-index: 1;
19
+
20
+ &.isCollapsed {
21
+ display: none;
22
+ }
23
+ }
24
+
25
+ .ulElem {
26
+ padding: 0;
27
+ margin: 0;
28
+ list-style: none;
29
+ }
30
+
31
+ .submenuTitle {
32
+ composes: g-type-label from global;
33
+ margin: 12px 0 8px 0;
34
+ color: var(--gray-3);
35
+ }
36
+
37
+ .divider {
38
+ margin: 12px 0;
39
+ border: 0;
40
+ padding: 0;
41
+ height: 1px;
42
+ background: var(--gray-5);
43
+ }
44
+
45
+ .submenuItem {
46
+ composes: g-type-body-small-strong from global;
47
+ display: block;
48
+ padding: 4px 0;
49
+ line-height: 1.6em;
50
+ color: var(--black);
51
+ }
@@ -0,0 +1,25 @@
1
+ import type { FC, ReactElement, ReactNode } from 'react'
2
+ import LinkWrap from '@dbosoft/react-uicore/link-wrap'
3
+ import Link from 'next/link'
4
+
5
+
6
+ const TitleLink: FC<{
7
+ text: string,
8
+ url: string,
9
+ children?: ReactNode
10
+ }> = ({
11
+ text,
12
+ url, children }) => {
13
+ return (
14
+ <LinkWrap
15
+ Link={Link}
16
+ className="font-bold text-gray-900 tracking-widest text-lg inline"
17
+ href={url}
18
+ title={text}
19
+ >{text}
20
+ {children}
21
+ </LinkWrap>
22
+ )
23
+ }
24
+
25
+ export default TitleLink
@@ -0,0 +1,29 @@
1
+ import classNames from 'classnames'
2
+ import React from 'react'
3
+
4
+ /**
5
+ *
6
+ * A span of styled text with
7
+ * an active state that adds a thick
8
+ * bottom border.
9
+ *
10
+ */
11
+ function NavItemText({
12
+ isActive,
13
+ text,
14
+ }: {
15
+ /** If true, item will be rendered with a thick bottom border below the text. */
16
+ isActive: boolean
17
+ /** Plain text to render within the styled <span /> */
18
+ text: string
19
+ }): React.ReactElement {
20
+ return (
21
+ <span
22
+ className={classNames("text-black text-sm font-semibold", isActive ? "border-b-2 pb-2 border-black" : "")}
23
+ >
24
+ {text}
25
+ </span>
26
+ )
27
+ }
28
+
29
+ export default NavItemText
@@ -0,0 +1,19 @@
1
+ .root {
2
+ color: var(--black);
3
+ position: relative;
4
+
5
+ &::after {
6
+ content: '';
7
+ position: absolute;
8
+ left: 0;
9
+ right: 0;
10
+ bottom: 0;
11
+ height: 2px;
12
+ background: var(--black);
13
+ opacity: 0;
14
+ }
15
+
16
+ &.isActive::after {
17
+ opacity: 1;
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ .root {
2
+ --gray-6-transparent: rgba(174, 176, 183, 0.45);
3
+ position: sticky;
4
+ top: -1px;
5
+ z-index: 800;
6
+ border-top: 1px solid var(--gray-6-transparent);
7
+ border-bottom: 1px solid transparent;
8
+ transition: border-bottom-color 0.8s;
9
+
10
+ &.isSticky {
11
+ border-bottom-color: var(--gray-6-transparent);
12
+ }
13
+ }
@@ -0,0 +1,27 @@
1
+ import { useState, useEffect, useRef, MutableRefObject } from 'react'
2
+
3
+ export default function useScrollLeft(): [
4
+ scrollRef: MutableRefObject<$TSFixMe>,
5
+ scrollLeft: number
6
+ ] {
7
+ const scrollRef = useRef(null)
8
+ const [scrollLeft, setScrollLeft] = useState()
9
+
10
+ useEffect(() => {
11
+ const scrollElem = scrollRef.current as $TSFixMe;
12
+ // Handler to call when scrollLeft may be affected
13
+ function handleScroll() {
14
+ if (!scrollRef.current) return null
15
+ // Set scroll data to state
16
+ setScrollLeft((scrollRef.current as $TSFixMe).scrollLeft)
17
+ }
18
+ // Add event listener
19
+ scrollElem.addEventListener('scroll', handleScroll)
20
+ // Call handler right away so state gets updated with initial scroll position
21
+ handleScroll()
22
+ // Remove event listener on cleanup
23
+ return () => scrollElem.removeEventListener('scroll', handleScroll)
24
+ }, [scrollRef])
25
+
26
+ return [scrollRef, 0]
27
+ }
@@ -0,0 +1,33 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ // https://usehooks.com/useWindowSize/
4
+ export default function useWindowSize() {
5
+ // Initialize state with undefined width/height so server and client renders match
6
+ // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
7
+ const [windowSize, setWindowSize] = useState({
8
+ width: undefined,
9
+ height: undefined,
10
+ })
11
+
12
+ useEffect(() => {
13
+ // Handler to call on window resize
14
+ function handleResize() {
15
+ // Set window width/height to state
16
+ setWindowSize({
17
+ width: window.innerWidth,
18
+ height: window.innerHeight,
19
+ })
20
+ }
21
+
22
+ // Add event listener
23
+ window.addEventListener('resize', handleResize)
24
+
25
+ // Call handler right away so state gets updated with initial window size
26
+ handleResize()
27
+
28
+ // Remove event listener on cleanup
29
+ return () => window.removeEventListener('resize', handleResize)
30
+ }, []) // Empty array ensures that effect is only run on mount
31
+
32
+ return windowSize
33
+ }
@@ -0,0 +1 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 18l6-6-6-6" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -0,0 +1 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" stroke="var(--gray-4)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path clip-rule="evenodd" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="var(--gray-4)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 17.387a.379.379 0 100-.758.379.379 0 000 .758z" fill="var(--gray-4)" stroke="var(--gray-4)" stroke-width="1.5" stroke-linejoin="round"/></svg>
@@ -0,0 +1,102 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { TabTriggerType } from './partials/tab-trigger'
3
+ import TabTriggers from './partials/tab-triggers'
4
+ import TabProvider, { useTabGroups } from './provider'
5
+ import s from './style.module.css'
6
+
7
+ interface TabChildProps {
8
+ /** Renders the tab contents. */
9
+ children: React.ReactElement
10
+ /** Plain text used for the tab heading. */
11
+ heading: string
12
+ /** Accepts a string such that, when the tab is active, other Tab elements outside the instance with a matching `group` value will automatically be shown. Note that `TabProvider` is required in order for this feature to function.v */
13
+ group?: string
14
+ /** Plain text displayed in a tooltip beside the tab heading. */
15
+ tooltip?: string
16
+ }
17
+
18
+ function Tab({ children }: TabChildProps): React.ReactElement {
19
+ return <>{children}</>
20
+ }
21
+
22
+ interface TabsProps {
23
+ /** Children to be displayed as tabs. Each child accepts the props `{ children: ReactElement, heading: string, tooltip?: string, group?: string }` */
24
+ children: Array<React.ReactElement<TabChildProps>>
25
+ /** If set to true, the tabs are centered in their container. Default is left-aligned. */
26
+ centered?: boolean
27
+ /** Optional className to add to the root element. */
28
+ className?: string
29
+ /** Set the default tab by its index. If not set, or if out of range, will default to the first tab, at index 0. */
30
+ defaultTabIdx?: number
31
+ /** If set to true, the bottom border underneath the tabs will fill the available width. Default border fills the constrained `.g-grid-container`. */
32
+ fullWidthBorder?: boolean
33
+ /** Optional callback which is executed when a new tab is selected. */
34
+ onChange?: (targetTabIdx: number, targetTabGroup?: string) => void
35
+ }
36
+
37
+ function Tabs({
38
+ children,
39
+ className,
40
+ defaultTabIdx = 0,
41
+ centered = false,
42
+ fullWidthBorder = false,
43
+ onChange,
44
+ }: TabsProps): React.ReactElement {
45
+ // Ensures a single child object converts to an array
46
+ children = Array.prototype.concat(children)
47
+
48
+ const isDefaultOutOfBounds =
49
+ defaultTabIdx >= children.length || defaultTabIdx < 0
50
+
51
+ const [activeTabIdx, setActiveTabIdx] = useState(
52
+ // if specified default is out of bounds (ie, it's determined at runtime),
53
+ // fallback to 0 to avoid throwing an error
54
+ isDefaultOutOfBounds ? 0 : defaultTabIdx
55
+ )
56
+ const groupCtx = useTabGroups()
57
+
58
+ function setActiveTab(targetIdx : number, groupId?:string) {
59
+ setActiveTabIdx(targetIdx)
60
+ if (onChange) onChange(targetIdx, groupId)
61
+ if (groupCtx) groupCtx.setActiveTabGroup(groupId)
62
+ }
63
+
64
+ useEffect(() => {
65
+ const hasGroups = children.filter((tab) => tab.props.group).length > 0
66
+ if (
67
+ process.env.NODE_ENV !== 'production' &&
68
+ hasGroups &&
69
+ groupCtx === undefined
70
+ ) {
71
+ console.warn(
72
+ '@dbosoft/react-tabs: The `TabProvider` cannot be accessed. Make sure it wraps the `Tabs` components so Tab Groups can work properly.'
73
+ )
74
+ }
75
+ }, [children, groupCtx])
76
+
77
+ if (!children) {
78
+ process.env.NODE_ENV !== 'production' &&
79
+ console.warn(
80
+ '@dbosoft/react-tabs: There are no `Tab` children for the `Tabs` component to render.'
81
+ )
82
+ }
83
+
84
+ return (
85
+ <section className={className}>
86
+ <TabTriggers
87
+ tabs={children.map((tab, index) => {
88
+ const { heading, group, tooltip } = tab.props
89
+ return { index, heading, group, tooltip } as TabTriggerType
90
+ })}
91
+ centered={centered}
92
+ fullWidthBorder={fullWidthBorder}
93
+ activeTabIdx={activeTabIdx}
94
+ setActiveTab={setActiveTab}
95
+ />
96
+ <div className={s.content}>{children[activeTabIdx].props.children}</div>
97
+ </section>
98
+ )
99
+ }
100
+
101
+ export default Tabs
102
+ export { TabProvider, useTabGroups, Tab }
@@ -0,0 +1,66 @@
1
+ import React, { useEffect } from 'react'
2
+ import TooltipIcon from '../../icons/tooltip.svg'
3
+ import Tooltip from '../tooltip'
4
+ import { useTabGroups } from '../../provider.js'
5
+ import s from './style.module.scss'
6
+ import classNames from 'classnames'
7
+
8
+ export interface TabTriggerType {
9
+ index: number
10
+ group: string
11
+ heading: string
12
+ tooltip?: string
13
+ }
14
+
15
+ interface TabTriggerProps {
16
+ tab: TabTriggerType
17
+ hasOverflow: boolean
18
+ activeTabIdx: number
19
+ setActiveTab: (tabIndex: number, tabGroup?: string) => void
20
+ }
21
+
22
+ function TabTrigger({
23
+ tab,
24
+ hasOverflow,
25
+ activeTabIdx,
26
+ setActiveTab,
27
+ }: TabTriggerProps): React.ReactElement {
28
+ const groupCtx = useTabGroups()
29
+ const activeGroup = groupCtx?.activeTabGroup
30
+ const isInActiveGroup = groupCtx && tab.group && tab.group === activeGroup
31
+ const isActiveIndex = tab.index === activeTabIdx
32
+ const isActiveTab = isInActiveGroup || isActiveIndex ? true : false
33
+
34
+ useEffect(() => {
35
+ // if the tab is active based on group and the
36
+ // index doesn't match, update the active index
37
+ if (isInActiveGroup) !isActiveIndex && setActiveTab(tab.index)
38
+ }, [isInActiveGroup, isActiveIndex, setActiveTab, tab.index])
39
+
40
+ return (
41
+ <button
42
+ className={classNames(s.root, {
43
+ [s.isActiveTab]: isActiveTab,
44
+ [s.hasOverflow]: hasOverflow,
45
+ })}
46
+ data-tabindex={tab.index}
47
+ onMouseDown={(e) => e.preventDefault()}
48
+ onClick={() => setActiveTab(tab.index, tab.group)}
49
+ >
50
+ <span className={s.inner}>
51
+ <span className="g-type-body-strong">{tab.heading}</span>
52
+ {tab.tooltip && (
53
+ <Tooltip label={tab.tooltip} aria-label={tab.tooltip}>
54
+ <span
55
+ data-testid="tooltip-icon"
56
+ className={s.tooltipTrigger}
57
+ dangerouslySetInnerHTML={{ __html: TooltipIcon }}
58
+ />
59
+ </Tooltip>
60
+ )}
61
+ </span>
62
+ </button>
63
+ )
64
+ }
65
+
66
+ export default TabTrigger
@@ -0,0 +1,69 @@
1
+ .root {
2
+ --inner-text-color: var(--gray-3);
3
+ --inner-border-color: transparent;
4
+ --inner-decoration: none;
5
+
6
+ display: flex;
7
+ align-items: center;
8
+ background: none;
9
+ border: none;
10
+ outline: none;
11
+ padding: 0 16px;
12
+
13
+ @media (--medium-up) {
14
+ padding: 0 24px;
15
+ }
16
+
17
+ &:first-of-type {
18
+ padding-left: 0;
19
+ }
20
+
21
+ &:last-of-type {
22
+ padding-right: 0;
23
+
24
+ /* With overflow, add
25
+ a bit of space to ensure tooltips
26
+ are still easily hover-able */
27
+ &.hasOverflow {
28
+ padding-right: 24px;
29
+ }
30
+ }
31
+
32
+ &:hover {
33
+ cursor: pointer;
34
+
35
+ --inner-text-color: var(--black);
36
+ }
37
+
38
+ &:focus {
39
+ --inner-text-color: var(--black);
40
+ --inner-decoration: underline;
41
+ }
42
+
43
+ &.isActiveTab {
44
+ --inner-border-color: var(--black);
45
+ --inner-text-color: var(--black);
46
+ }
47
+ }
48
+
49
+ .inner {
50
+ display: flex;
51
+ align-items: center;
52
+ padding: 18px 0 15px 0;
53
+ border-bottom: 3px solid;
54
+ border-color: var(--inner-border-color);
55
+ color: var(--inner-text-color);
56
+ transition: color 0.2s;
57
+ white-space: nowrap;
58
+ text-decoration: var(--inner-decoration);
59
+ }
60
+
61
+ .tooltipTrigger {
62
+ margin-left: 0.5rem;
63
+ margin-top: 1px;
64
+ width: 1.125rem;
65
+
66
+ & svg {
67
+ width: 100%;
68
+ }
69
+ }