diff --git a/app/components/Article/Contents.tsx b/app/components/Article/Contents.tsx index cba8c768..c0ae2d26 100644 --- a/app/components/Article/Contents.tsx +++ b/app/components/Article/Contents.tsx @@ -2,6 +2,7 @@ import {useRef, useEffect} from 'react' import useIsMobile from '~/hooks/isMobile' import {questionUrl} from '~/routesMapper' import type {Glossary, PageId, GlossaryEntry} from '~/server-utils/stampy' +import {togglePopup} from '../popups' const footnoteHTML = (el: HTMLDivElement, e: HTMLAnchorElement): string | null => { const id = e.getAttribute('href') || '' @@ -19,19 +20,12 @@ const footnoteHTML = (el: HTMLDivElement, e: HTMLAnchorElement): string | null = return elem.innerHTML } -const scrollToElement = (e: HTMLElement, offset?: number) => { - const elementPosition = e.getBoundingClientRect().top + window.pageYOffset - const offsetPosition = elementPosition - (offset || 0) - - window.scrollTo({top: offsetPosition, behavior: 'smooth'}) -} - const addPopup = (e: HTMLElement, id: string, contents: string, mobile?: boolean): HTMLElement => { const preexisting = document.getElementById(id) if (preexisting) return preexisting const popup = document.createElement('div') - popup.className = 'link-popup bordered small' + popup.className = 'link-popup bordered small background' popup.innerHTML = contents popup.id = id @@ -43,14 +37,9 @@ const addPopup = (e: HTMLElement, id: string, contents: string, mobile?: boolean popup.addEventListener('mouseover', () => popup.classList.add('shown')) popup.addEventListener('mouseout', () => popup.classList.remove('shown')) } else { - const togglePopup = (event: Event) => { - event.preventDefault() - popup.classList.toggle('shown') - document.body.classList.toggle('noscroll') - scrollToElement(e, 16) - } - popup.addEventListener('click', togglePopup) - e.addEventListener('click', togglePopup) + const toggle = () => popup.classList.toggle('shown') + popup.addEventListener('click', togglePopup(toggle, e)) + e.addEventListener('click', togglePopup(toggle, e)) } return popup diff --git a/app/components/Article/article.css b/app/components/Article/article.css index ed5b1d8f..2163894d 100644 --- a/app/components/Article/article.css +++ b/app/components/Article/article.css @@ -102,10 +102,7 @@ article .glossary-entry { article .link-popup { visibility: hidden; - opacity: 0; z-index: 4; - position: absolute; - top: 0; width: 512px; } @@ -218,25 +215,14 @@ article .banner h3 .title { flex-direction: row; flex-wrap: wrap; } + + article .link-popup { + width: 100%; + } article .footer-comtainer > div:nth-child(-n + 2) { flex: 1 1; } article { margin: 0; } - - article .link-popup { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: #ff000033; - overflow: scroll; - } - article .link-popup .footnote { - margin: auto; - background-color: white; - width: 80%; - } } diff --git a/app/components/ArticlesDropdown/dropdown.css b/app/components/ArticlesDropdown/dropdown.css index bb39a400..745f3af1 100644 --- a/app/components/ArticlesDropdown/dropdown.css +++ b/app/components/ArticlesDropdown/dropdown.css @@ -5,7 +5,6 @@ } .articles-dropdown-container { - z-index: 100; top: 80px; position: absolute; justify-content: space-between; diff --git a/app/components/ArticlesDropdown/index.tsx b/app/components/ArticlesDropdown/index.tsx index a98ba688..fb8d46b6 100644 --- a/app/components/ArticlesDropdown/index.tsx +++ b/app/components/ArticlesDropdown/index.tsx @@ -8,11 +8,50 @@ import Button from '~/components/Button' import './dropdown.css' import useIsMobile from '~/hooks/isMobile' +type LinkProps = { + to: string + text: string + pageid?: string + className?: string + onClick: () => void +} +const Link = ({to, text, pageid, className, onClick}: LinkProps) => ( +
+ + {text} + +
+) + +type ArticlesSectionProps = { + category: Category + toc: TOCItem[] + className?: string + hide: () => void +} +const ArticlesSection = ({category, toc, className, hide}: ArticlesSectionProps) => ( +
+
{category} sections
+ {toc + .filter((item) => item.category === category) + .map((item: TOCItem) => ( + + ))} +
+) + export type ArticlesDropdownProps = { toc: TOCItem[] categories: Tag[] + fullWidth?: boolean } -export const ArticlesDropdown = ({toc, categories}: ArticlesDropdownProps) => { +export const ArticlesDropdown = ({toc, categories, fullWidth}: ArticlesDropdownProps) => { // The dropdown works by using the onHover pseudoclass, so will only hide once // the mouse leaves it. When using client side changes, the mouse doesn't leave // it, so it's always shown (until the mouse is moved out, of course). To get around @@ -22,63 +61,26 @@ export const ArticlesDropdown = ({toc, categories}: ArticlesDropdownProps) => { const hide = () => setHidden(true) useEffect(() => setHidden(false), [hidden]) const mobile = useIsMobile() - const Link = ({ - to, - text, - pageid, - className, - }: { - to: string - text: string - pageid?: string - className?: string - }) => ( -
- - {text} - -
- ) - - const ArticlesSection = ({ - category, - toc, - className, - }: { - category: Category - toc: TOCItem[] - className?: string - }) => ( -
-
{category} sections
- {toc - .filter((item) => item.category === category) - .map((item: TOCItem) => ( - - ))} -
- ) return hidden ? null : ( -
-
+
+
-
+
{/*sorted right side*/}
Browse by category
@@ -87,7 +89,13 @@ export const ArticlesDropdown = ({toc, categories}: ArticlesDropdownProps) => { ?.sort(sortFuncs['by number of questions']) .slice(0, 12) .map((tag) => ( - + ))}
diff --git a/app/components/Button/button.css b/app/components/Button/button.css index b44d9267..0f4a5060 100644 --- a/app/components/Button/button.css +++ b/app/components/Button/button.css @@ -183,6 +183,7 @@ /* #### Composite button #### */ .composite-button { cursor: pointer; + display: flex; } .composite-button > form .button, diff --git a/app/components/Button/index.tsx b/app/components/Button/index.tsx index c578dfcd..2183cfdd 100644 --- a/app/components/Button/index.tsx +++ b/app/components/Button/index.tsx @@ -26,6 +26,8 @@ const Button = ({ (secondary && 'button-secondary') || 'button', className, tooltip && !secondary && 'tooltip', + secondary && active && 'active', + secondary && !active && !disabled && 'inactive', ] .filter((i) => i) .join(' ') @@ -33,7 +35,7 @@ const Button = ({ return ( { if (disabled) { @@ -49,7 +51,7 @@ const Button = ({ } return ( -
+type MenuItemProps = { + action: string | (() => void) + label: string + Icon: ElementType + hasChildren?: boolean +} +const MenuItem = ({action, label, Icon, hasChildren}: MenuItemProps) => ( + +) + +const SearchBar = ({onClose}: {show?: boolean; onClose: () => void}) => ( +
+ + +
+) + +type MenuProps = { + state: State + setState: (state: State) => void +} +const Menu = ({state, setState}: MenuProps) => { + const isMenu = state === 'menu' + const isInitial = state === 'initial' + const MenuIcon = isMenu ? XLarge : ListLarge + return ( + (isInitial || isMenu) && ( +
+
+ + + + + {isInitial && ( + setState('search')} /> )} - - )} - {showArticles && ( - <> - + setState(isInitial ? 'menu' : 'initial')} /> +
-
- -
- - )} -
+ {isMenu && ( + setState('articles')} + /> + )} + {isMenu && } + + ) ) } + +export const MobileNav = ({toc, categories}: NavProps) => { + const [current, setCurrent] = useState('initial') + if (['initial', 'menu'].includes(current)) { + return + } else if (current === 'search') { + return setCurrent('initial')} /> + } else { + return ( +
+ + +
setCurrent('initial')}> + +
+
+ ) + } +} export default MobileNav diff --git a/app/components/Nav/Mobile/navMobile.css b/app/components/Nav/Mobile/navMobile.css index 2d2fd9bc..3d8c3b17 100644 --- a/app/components/Nav/Mobile/navMobile.css +++ b/app/components/Nav/Mobile/navMobile.css @@ -1,96 +1,53 @@ @media only screen and (max-width: 780px) { - .top-header { - padding: 0; - } - .top-nav { - justify-content: space-between; - padding: var(--spacing-16); - } - .top-logo { - padding: 0; - } - .composite-elements { + .flex-row { display: flex; + flex-direction: row; align-items: center; - } - .space-between { justify-content: space-between; + gap: var(--spacing-8); } - .flex-start { - justify-content: flex-start; - } - .mobile-menu > .button { - border-radius: 0; - padding: var(--spacing-80) var(--spacing-16); - } - .mobile-menu > .button:first-child { - border-bottom: 0; - } - .icon-margin { - margin-right: var(--spacing-8); - } - .articles-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-16); - border-bottom: 1px solid var(--colors-cool-grey-200); - } - .back-icon { - cursor: pointer; - transform: rotate(180deg); - } - .articles-mobile { - padding: var(--spacing-24); + + .top-header { + padding: var(--spacing-8) 0; } - .articles-dropdown-container { - visibility: visible !important; - flex-direction: column; - border: 0; + + .top-header .menu-item { box-shadow: none; - position: static !important; - padding: 0 !important; - width: 100% !important; - } - .search-icon { - margin-right: var(--spacing-16); - } - .search-content > svg:nth-of-type(2) { - visibility: hidden; + height: 80px; + border-bottom: solid 1px var(--colors-cool-grey-200); + border-radius: 0; + padding: 0 24px; + gap: var(--spacing-16); } - .mobile-searchbar { + + .top-header.background { + background-color: var(--colors-white); display: flex; - flex-direction: row-reverse; - flex-wrap: nowrap; - align-content: center; justify-content: flex-start; align-items: flex-start; + flex-direction: column; + } + .top-header.background > * { + margin: inherit; width: 100%; } - .mobile-searchbar > svg { - margin-top: var(--spacing-12); - margin-left: var(--spacing-16); + + .search-bar { + padding: var(--spacing-24) var(--spacing-40); + gap: var(--spacing-16); } - .mobile-searchbar > * .search-box.expandable:focus-within, - .mobile-searchbar > * .search-box.expandable { + .search-bar .search-box:focus-within, + .search-bar .search-box { width: 100%; } - .container-search-results-mobile { - height: 50vh; - width: 100%; - position: inherit; - margin-top: var(--spacing-8); - overflow: scroll; + + .container-search-results { + width: calc(100% - 48px); } - .mobile-searchbar > div { + + .articles-mobile .articles-dropdown-container { + visibility: visible; + flex-direction: column; width: 100%; } - .expanded { - position: fixed; - z-index: 100; - height: 100vh; - width: 100vw; - background: var(--colors-cool-grey-100); - overflow: scroll; - } } diff --git a/app/components/SearchResults/Dropdown.tsx b/app/components/SearchResults/Dropdown.tsx index 16231c87..3ab2bc48 100644 --- a/app/components/SearchResults/Dropdown.tsx +++ b/app/components/SearchResults/Dropdown.tsx @@ -22,19 +22,25 @@ export interface SearchResultsProps { url: string } -export const SearchResults = ({results}: {results: SearchResultsProps[]}) => { +export const SearchResults = ({ + results, + onSelect, +}: { + results: SearchResultsProps[] + onSelect?: () => void +}) => { const isMobile = useIsMobile() const noResults = results.length === 0 if (noResults) { return ( -
+
No results found
) } return ( -
+
{results.map((result, i) => ( diff --git a/app/components/popups.ts b/app/components/popups.ts new file mode 100644 index 00000000..3a8fdc52 --- /dev/null +++ b/app/components/popups.ts @@ -0,0 +1,14 @@ +export const scrollToElement = (e: HTMLElement, offset?: number) => { + const elementPosition = e.getBoundingClientRect().top + window.pageYOffset + const offsetPosition = elementPosition - (offset || 0) + + window.scrollTo({top: offsetPosition, behavior: 'smooth'}) +} + +export const togglePopup = + (onToggle: () => void, reference?: HTMLElement) => (event: MouseEvent | React.MouseEvent) => { + event.preventDefault() + onToggle() + document.body.classList.toggle('noscroll') + reference && scrollToElement(reference, 16) + } diff --git a/app/components/search.tsx b/app/components/search.tsx index ca776976..8a795612 100644 --- a/app/components/search.tsx +++ b/app/components/search.tsx @@ -8,9 +8,10 @@ import useOutsideOnClick from '~/hooks/useOnOutsideClick' type Props = { limitFromUrl?: number + className?: string } -export default function Search({limitFromUrl}: Props) { +export default function Search({limitFromUrl, className}: Props) { const [showResults, setShowResults] = useState(false) const [searchPhrase, setSearchPhrase] = useState('') @@ -33,7 +34,7 @@ export default function Search({limitFromUrl}: Props) { const handleChange = debounce(searchFn, 100) return ( -
setShowResults(true)} ref={clickDetectorRef}> +
setShowResults(true)} ref={clickDetectorRef}>
{isPendingSearch && results.length == 0 && ( @@ -41,6 +42,7 @@ export default function Search({limitFromUrl}: Props) { )} {!isPendingSearch && searchPhrase && showResults && ( setShowResults(false)} results={results.map((r) => ({ title: r.title, url: questionUrl(r), diff --git a/app/root.css b/app/root.css index 209b275f..a2b14600 100644 --- a/app/root.css +++ b/app/root.css @@ -459,6 +459,12 @@ svg { .flex-double { flex-grow: 2; } +.auto-left { + margin-left: auto; +} +.auto-right { + margin-right: auto; +} .centered { margin: auto; @@ -470,6 +476,11 @@ svg { margin: 0 auto; } +.reverse-icon { + cursor: pointer; + transform: rotate(180deg); +} + .pointer { cursor: pointer; } @@ -603,6 +614,22 @@ svg { padding: var(--spacing-32); flex-direction: column; } + + .background { + position: fixed; + top: 0; + left: 0; + background-color: #1b2b3e99; + width: 100vw; + height: 100vh; + overflow: scroll; + } + + .background > * { + margin: 198px auto; + background-color: var(--colors-white); + width: 80%; + } } /* end mobile */ p, textarea,