Merge pull request #150 from jedmund/scroll-indicator

Add scroll indicators to new scrollable modals
This commit is contained in:
Justin Edmund 2023-01-22 21:33:57 -08:00 committed by GitHub
commit b6d239121d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 446 additions and 212 deletions

View file

@ -2,13 +2,19 @@
gap: 0;
padding-bottom: $unit;
& > div:not(.DialogHeader) {
.content {
display: flex;
flex-direction: column;
gap: $unit-2x;
padding: 0 $unit-4x;
}
.sections {
display: flex;
flex-direction: column;
gap: $unit-2x;
}
section {
margin-bottom: $unit;

View file

@ -19,6 +19,7 @@ import './index.scss'
const AboutModal = () => {
const { t } = useTranslation('common')
const headerRef = React.createRef<HTMLDivElement>()
return (
<Dialog>
@ -29,10 +30,11 @@ const AboutModal = () => {
</DialogTrigger>
<DialogContent
className="About"
headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}}
>
<div className="DialogHeader">
<div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">{t('menu.about')}</DialogTitle>
<DialogClose className="DialogClose" asChild>
<span>
@ -41,7 +43,7 @@ const AboutModal = () => {
</DialogClose>
</div>
<div>
<div className="content">
<section>
<p>
Granblue.team is a tool to save and share team comps for{' '}

View file

@ -3,6 +3,7 @@
flex-direction: column;
gap: $unit-2x;
width: $unit * 64;
overflow-y: hidden;
.Fields {
display: flex;

View file

@ -85,6 +85,10 @@ const AccountModal = (props: Props) => {
const [languageOpen, setLanguageOpen] = useState(false)
const [themeOpen, setThemeOpen] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// UI management
function openChange(open: boolean) {
setOpen(open)
@ -286,10 +290,12 @@ const AccountModal = (props: Props) => {
</DialogTrigger>
<DialogContent
className="Account"
headerref={headerRef}
footerref={footerRef}
onOpenAutoFocus={(event: Event) => {}}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader">
<div className="DialogHeader" ref={headerRef}>
<div className="DialogTop">
<DialogTitle className="SubTitle">
{t('modals.settings.title')}
@ -310,7 +316,7 @@ const AccountModal = (props: Props) => {
{languageField()}
{themeField()}
</div>
<div className="DialogFooter">
<div className="DialogFooter" ref={footerRef}>
<Button
contained={true}
text={t('modals.settings.buttons.confirm')}

View file

@ -2,7 +2,7 @@
align-items: center;
display: flex;
justify-content: center;
position: absolute;
position: fixed;
height: 100vh;
width: 100vw;
top: 0;
@ -11,6 +11,8 @@
}
.Alert {
animation: $duration-modal-open cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none
running openModalDesktop;
background: var(--dialog-bg);
border-radius: $unit;
display: flex;

View file

@ -1,8 +1,7 @@
.Changelog.DialogContent {
gap: 0;
padding-bottom: $unit-4x;
& > div:not(.DialogHeader) {
.updates {
padding: 0 $unit-4x;
}
@ -10,6 +9,7 @@
display: flex;
flex-direction: column;
gap: $unit-4x;
margin-bottom: $unit-4x;
}
.version {
@ -30,6 +30,11 @@
display: grid;
grid-template-rows: 1fr auto;
gap: $unit;
& > h4 {
font-weight: $medium;
font-size: $font-regular;
}
}
.items {
@ -66,6 +71,7 @@
li {
margin-bottom: $unit-half;
font-size: $font-regular;
}
}
}

View file

@ -16,6 +16,7 @@ import './index.scss'
const ChangelogModal = () => {
const { t } = useTranslation('common')
const headerRef = React.createRef<HTMLDivElement>()
return (
<Dialog>
@ -26,10 +27,12 @@ const ChangelogModal = () => {
</DialogTrigger>
<DialogContent
className="Changelog"
title={t('menu.changelog')}
headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}}
>
<div className="DialogHeader">
<div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">
{t('menu.changelog')}
</DialogTitle>

View file

@ -30,6 +30,9 @@ const CharacterConflictModal = (props: Props) => {
// States
const [open, setOpen] = useState(false)
// Refs
const footerRef = React.createRef<HTMLDivElement>()
useEffect(() => {
setOpen(props.open)
}, [setOpen, props.open])
@ -73,42 +76,52 @@ const CharacterConflictModal = (props: Props) => {
<Dialog open={open} onOpenChange={openChange}>
<DialogContent
className="Conflict"
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close}
>
<p>
<Trans i18nKey="modals.conflict.character"></Trans>
</p>
<div className="CharacterDiagram Diagram">
<ul>
{props.conflictingCharacters?.map((character, i) => (
<li className="character" key={`conflict-${i}`}>
<div className="Content">
<p>
<Trans i18nKey="modals.conflict.character"></Trans>
</p>
<div className="CharacterDiagram Diagram">
<ul>
{props.conflictingCharacters?.map((character, i) => (
<li className="character" key={`conflict-${i}`}>
<img
alt={character.object.name[locale]}
src={imageUrl(character.object, character.uncap_level)}
/>
<span>{character.object.name[locale]}</span>
</li>
))}
</ul>
<span className="arrow">&rarr;</span>
<div className="wrapper">
<div className="character">
<img
alt={character.object.name[locale]}
src={imageUrl(character.object, character.uncap_level)}
alt={props.incomingCharacter?.name[locale]}
src={imageUrl(props.incomingCharacter)}
/>
<span>{character.object.name[locale]}</span>
</li>
))}
</ul>
<span className="arrow">&rarr;</span>
<div className="wrapper">
<div className="character">
<img
alt={props.incomingCharacter?.name[locale]}
src={imageUrl(props.incomingCharacter)}
/>
<span>{props.incomingCharacter?.name[locale]}</span>
<span>{props.incomingCharacter?.name[locale]}</span>
</div>
</div>
</div>
</div>
<footer>
<Button onClick={close} text={t('buttons.cancel')} />
<Button
onClick={props.resolveConflict}
text={t('modals.conflict.buttons.confirm')}
/>
</footer>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<Button
contained={true}
onClick={close}
text={t('buttons.cancel')}
/>
<Button
contained={true}
onClick={props.resolveConflict}
text={t('modals.conflict.buttons.confirm')}
/>
</div>
</div>
</DialogContent>
<Overlay open={open} visible={true} />
</Dialog>

View file

@ -35,7 +35,7 @@
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: 0 $unit-4x;
padding: 0 $unit-4x $unit-2x;
section {
display: flex;

View file

@ -83,11 +83,17 @@ const CharacterModal = ({
const [open, setOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Classes
const headerClasses = classNames({
DialogHeader: true,
Scrolled: scrolled,
Short: true,
})
// Callbacks and Hooks
useEffect(() => {
setOpen(modalOpen)
}, [modalOpen])
@ -281,10 +287,12 @@ const CharacterModal = ({
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="Character"
headerref={headerRef}
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}}
>
<div className={headerClasses}>
<div className={headerClasses} ref={headerRef}>
<img
alt={gridCharacter.object.name[locale]}
className="DialogImage"
@ -311,7 +319,7 @@ const CharacterModal = ({
{earringSelect()}
{awakeningSelect()}
</div>
<div className="DialogFooter">
<div className="DialogFooter" ref={footerRef}>
<Button
contained={true}
onClick={updateCharacter}

View file

@ -1,6 +1,4 @@
.Dialog {
// animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
// openModalDesktop;
position: fixed;
background: none;
border: 0;
@ -16,9 +14,13 @@
.DialogContent {
$multiplier: 4;
animation: $duration-modal-open cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal
none running openModalDesktop;
background: var(--dialog-bg);
border-radius: $card-corner;
box-sizing: border-box;
border: 0.5px solid rgba(0, 0, 0, 0.18);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
display: flex;
flex-direction: column;
gap: $unit * $multiplier;
@ -26,6 +28,7 @@
min-width: $unit * 48;
// min-height: $unit-12x;
overflow-y: scroll;
// height: 80vh;
max-height: 80vh;
min-width: 580px;
max-width: 42vw;
@ -49,8 +52,14 @@
width: 100%;
}
.Scrollable {
overflow-y: auto;
}
.DialogHeader {
background: var(--dialog-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0);
border-bottom: 1px solid rgba(0, 0, 0, 0);
display: flex;
align-items: center;
gap: $unit-2x;
@ -137,10 +146,25 @@
align-items: flex-end;
background: var(--dialog-bg);
bottom: 0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.16);
border-top: 1px solid rgba(0, 0, 0, 0.24);
display: flex;
flex-direction: column;
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
position: sticky;
.Buttons {
display: flex;
gap: $unit;
&.Span {
width: 100%;
.Button {
width: 100%;
}
}
}
}
.actions {
@ -152,16 +176,23 @@
&.Conflict {
$weapon-diameter: 14rem;
& > p {
line-height: 1.2;
max-width: 400px;
.Content {
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: $unit-4x $unit-4x $unit-2x $unit-4x;
strong {
font-weight: $bold;
}
&:lang(ja) {
& > p {
font-size: $font-regular;
line-height: 1.4;
strong {
font-weight: $bold;
}
&:lang(ja) {
line-height: 1.4;
}
}
}
@ -198,7 +229,7 @@
align-items: center;
display: flex;
flex-direction: column;
gap: $unit * 2;
gap: $unit-2x;
}
.wrapper {

View file

@ -1,15 +1,18 @@
import React from 'react'
import React, { useEffect } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import classNames from 'classnames'
import debounce from 'lodash.debounce'
import './index.scss'
import Overlay from '~components/Overlay'
import './index.scss'
interface Props
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {
headerref?: React.RefObject<HTMLDivElement>
footerref?: React.RefObject<HTMLDivElement>
onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void
}
@ -18,10 +21,106 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
{ children, ...props },
forwardedRef
) {
// Classes
const classes = classNames(props.className, {
DialogContent: true,
})
// Handlers
function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) {
const scrollTop = event.currentTarget.scrollTop
const scrollHeight = event.currentTarget.scrollHeight
const clientHeight = event.currentTarget.clientHeight
if (props.headerref && props.headerref.current)
manipulateHeaderShadow(props.headerref.current, scrollTop)
if (props.footerref && props.footerref.current)
manipulateFooterShadow(
props.footerref.current,
scrollTop,
scrollHeight,
clientHeight
)
}
function manipulateHeaderShadow(header: HTMLDivElement, scrollTop: number) {
const boxShadowBase = '0 2px 8px'
const maxValue = 50
if (scrollTop >= 0) {
const input = scrollTop > maxValue ? maxValue : scrollTop
const boxShadowOpacity = mapRange(input, 0, maxValue, 0.0, 0.16)
const borderOpacity = mapRange(input, 0, maxValue, 0.0, 0.24)
header.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
header.style.borderBottomColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
function manipulateFooterShadow(
footer: HTMLDivElement,
scrollTop: number,
scrollHeight: number,
clientHeight: number
) {
const boxShadowBase = '0 -2px 8px'
const minValue = scrollHeight - 200
const currentScroll = scrollTop + clientHeight
if (currentScroll >= minValue) {
const input = currentScroll < minValue ? minValue : currentScroll
const boxShadowOpacity = mapRange(
input,
minValue,
scrollHeight,
0.16,
0.0
)
const borderOpacity = mapRange(input, minValue, scrollHeight, 0.24, 0.0)
footer.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
footer.style.borderTopColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
const calculateFooterShadow = debounce(() => {
const boxShadowBase = '0 -2px 8px'
const scrollable = document.querySelector('.Scrollable')
const footer = props.footerref
if (footer && footer.current) {
if (scrollable && scrollable.clientHeight >= scrollable.scrollHeight) {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0)`
} else {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0.16)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0.24)`
}
}
}, 100)
useEffect(() => {
window.addEventListener('resize', calculateFooterShadow)
calculateFooterShadow()
return () => {
window.removeEventListener('resize', calculateFooterShadow)
}
}, [calculateFooterShadow])
function mapRange(
value: number,
low1: number,
high1: number,
low2: number,
high2: number
) {
return low2 + ((high2 - low2) * (value - low1)) / (high1 - low1)
}
return (
<DialogPrimitive.Portal>
<dialog className="Dialog">
@ -32,7 +131,9 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef}
>
{children}
<div className="Scrollable" onScroll={handleScroll}>
{children}
</div>
</DialogPrimitive.Content>
</dialog>
<Overlay visible={true} open={true} />

View file

@ -136,7 +136,7 @@ const JobSection = (props: Props) => {
) : (
''
)}
<div className="Overlay" />
<div className="Job Overlay" />
</div>
<div className="JobDetails">
{props.editable ? (

View file

@ -2,11 +2,10 @@
gap: $unit;
min-width: $unit * 52;
form {
.Fields {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
margin-bottom: $unit-3x;
padding: 0 $unit-3x;
gap: $unit;
padding: 0 $unit-4x;
}
}

View file

@ -9,7 +9,7 @@ import setUserToken from '~utils/setUserToken'
import { accountState } from '~utils/accountState'
import Button from '~components/Button'
import Input from '~components/LabelledInput'
import Input from '~components/Input'
import { Dialog, DialogTrigger, DialogClose } from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import changeLanguage from '~utils/changeLanguage'
@ -43,6 +43,7 @@ const LoginModal = () => {
// Set up form refs
const emailInput: React.RefObject<HTMLInputElement> = React.createRef()
const passwordInput: React.RefObject<HTMLInputElement> = React.createRef()
const footerRef: React.RefObject<HTMLDivElement> = React.createRef()
const form: React.RefObject<HTMLInputElement>[] = [emailInput, passwordInput]
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
@ -199,6 +200,7 @@ const LoginModal = () => {
</DialogTrigger>
<DialogContent
className="Login"
footerref={footerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
@ -212,29 +214,34 @@ const LoginModal = () => {
</div>
<form className="form" onSubmit={login}>
<Input
className="Bound"
name="email"
placeholder={t('modals.login.placeholders.email')}
onChange={handleChange}
error={errors.email}
ref={emailInput}
/>
<div className="Fields">
<Input
className="Bound"
name="email"
placeholder={t('modals.login.placeholders.email')}
onChange={handleChange}
error={errors.email}
ref={emailInput}
/>
<Input
className="Bound"
name="password"
placeholder={t('modals.login.placeholders.password')}
type="password"
onChange={handleChange}
error={errors.password}
ref={passwordInput}
/>
<Button
disabled={!formValid}
text={t('modals.login.buttons.confirm')}
/>
<Input
className="Bound"
name="password"
placeholder={t('modals.login.placeholders.password')}
type="password"
onChange={handleChange}
error={errors.password}
ref={passwordInput}
/>
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<Button
disabled={!formValid}
text={t('modals.login.buttons.confirm')}
/>
</div>
</div>
</form>
</DialogContent>
</Dialog>

View file

@ -6,6 +6,14 @@
right: 0;
bottom: 0;
left: 0;
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
animation: 0.24s ease-in fadeInFilter;
animation-fill-mode: forwards;
&.Job {
animation: none;
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(1);
}
&.Visible {
background: rgba(0, 0, 0, 0.6);

View file

@ -12,6 +12,7 @@ import CharacterGrid from '~components/CharacterGrid'
import api from '~utils/api'
import { appState, initialAppState } from '~utils/appState'
import { GridType } from '~utils/enums'
import { retrieveCookies } from '~utils/retrieveCookies'
import type { DetailsObject } from '~types'
import './index.scss'
@ -37,6 +38,9 @@ const Party = (props: Props) => {
const { party } = useSnapshot(appState)
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
// Retrieve cookies
const cookies = retrieveCookies()
// Reset state on first load
useEffect(() => {
const resetState = clonedeep(initialAppState)
@ -107,13 +111,17 @@ const Party = (props: Props) => {
}
// Deleting the party
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
function deleteTeam() {
if (appState.party.editable && appState.party.id) {
api.endpoints.parties
.destroy({ id: appState.party.id })
.then(() => {
// Push to route
router.push('/')
if (cookies && cookies.account.username) {
router.push(`/${cookies.account.username}`)
} else {
router.push('/')
}
// Clean state
const resetState = clonedeep(initialAppState)

View file

@ -9,7 +9,7 @@ import LiteYouTubeEmbed from 'react-lite-youtube-embed'
import classNames from 'classnames'
import reactStringReplace from 'react-string-replace'
import * as AlertDialog from '@radix-ui/react-alert-dialog'
import Alert from '~components/Alert'
import Button from '~components/Button'
import CharLimitedFieldset from '~components/CharLimitedFieldset'
@ -40,9 +40,7 @@ interface Props {
new: boolean
editable: boolean
updateCallback: (details: DetailsObject) => void
deleteCallback: (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => void
deleteCallback: () => void
}
const PartyDetails = (props: Props) => {
@ -60,6 +58,7 @@ const PartyDetails = (props: Props) => {
const [open, setOpen] = useState(false)
const [name, setName] = useState('')
const [alertOpen, setAlertOpen] = useState(false)
const [chargeAttack, setChargeAttack] = useState(true)
const [fullAuto, setFullAuto] = useState(false)
@ -293,6 +292,14 @@ const PartyDetails = (props: Props) => {
toggleDetails()
}
function handleClick() {
setAlertOpen(!alertOpen)
}
function deleteParty() {
props.deleteCallback()
}
function extractYoutubeVideoIds(text: string) {
// Initialize an array to store the video IDs
const videoIds = []
@ -381,42 +388,18 @@ const PartyDetails = (props: Props) => {
)
}
const deleteButton = () => {
const deleteAlert = () => {
if (party.editable) {
return (
<AlertDialog.Root>
<AlertDialog.Trigger className="Button Blended medium destructive">
<span className="Accessory">
<CrossIcon />
</span>
<span className="Text">{t('buttons.delete')}</span>
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" />
<AlertDialog.Content className="Dialog">
<AlertDialog.Title className="DialogTitle">
{t('modals.delete_team.title')}
</AlertDialog.Title>
<AlertDialog.Description className="DialogDescription">
{t('modals.delete_team.description')}
</AlertDialog.Description>
<div className="actions">
<AlertDialog.Cancel className="Button modal">
{t('modals.delete_team.buttons.cancel')}
</AlertDialog.Cancel>
<AlertDialog.Action
className="Button modal destructive"
onClick={(e) => props.deleteCallback(e)}
>
{t('modals.delete_team.buttons.confirm')}
</AlertDialog.Action>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
<Alert
open={alertOpen}
primaryAction={deleteParty}
primaryActionText={t('modals.delete_team.buttons.confirm')}
cancelAction={() => setAlertOpen(false)}
cancelActionText={t('modals.delete_team.buttons.cancel')}
message={t('modals.delete_team.description')}
/>
)
} else {
return ''
}
}
@ -553,7 +536,16 @@ const PartyDetails = (props: Props) => {
<div className="bottom">
<div className="left">
{router.pathname !== '/new' ? deleteButton() : ''}
{router.pathname !== '/new' ? (
<Button
accessoryIcon={<CrossIcon />}
className="Blended medium destructive"
onClick={handleClick}
text={t('buttons.delete')}
/>
) : (
''
)}
</div>
<div className="right">
<Button text={t('buttons.cancel')} onClick={toggleDetails} />
@ -662,6 +654,7 @@ const PartyDetails = (props: Props) => {
</div>
{readOnly}
{editable}
{deleteAlert()}
</section>
)
}

View file

@ -24,7 +24,7 @@
}
}
& > div:not(.DialogHeader) {
.content {
display: flex;
flex-direction: column;
gap: $unit-2x;

View file

@ -17,6 +17,7 @@ import './index.scss'
const RoadmapModal = () => {
const { t } = useTranslation('roadmap')
const headerRef = React.createRef<HTMLDivElement>()
return (
<Dialog>
@ -27,10 +28,12 @@ const RoadmapModal = () => {
</DialogTrigger>
<DialogContent
className="Roadmap"
title={t('title')}
headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}}
>
<div className="DialogHeader">
<div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">{t('title')}</DialogTitle>
<DialogClose className="DialogClose" asChild>
<span>
@ -39,7 +42,7 @@ const RoadmapModal = () => {
</DialogClose>
</div>
<div>
<div className="content">
<section className="notes">
<p>{t('blurb')}</p>
<p>{t('link.intro')}</p>

View file

@ -3,8 +3,6 @@
display: flex;
flex-direction: column;
min-height: 430px;
height: 480px;
gap: 0;
padding: 0;
@include breakpoint(phone) {
@ -14,17 +12,16 @@
min-height: 100vh;
}
#Header {
border-bottom: 1px solid transparent;
.DialogHeader.Search {
align-items: inherit;
display: flex;
flex-direction: column;
gap: $unit;
padding-bottom: $unit * 2;
&.scrolled {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
}
padding: 0;
padding-bottom: $unit-2x;
position: sticky;
top: 0;
left: 0;
#Bar {
align-items: center;
@ -63,7 +60,6 @@
#Results {
margin: 0;
max-height: 356px;
padding: 0 ($unit * 1.5);
overflow-y: scroll;

View file

@ -42,8 +42,10 @@ const SearchModal = (props: Props) => {
// Set up translation
const { t } = useTranslation('common')
let searchInput = React.createRef<HTMLInputElement>()
let scrollContainer = React.createRef<HTMLDivElement>()
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const searchInput = React.createRef<HTMLInputElement>()
const scrollContainer = React.createRef<HTMLDivElement>()
const [firstLoad, setFirstLoad] = useState(true)
const [filters, setFilters] = useState<{ [key: string]: any }>()
@ -356,10 +358,11 @@ const SearchModal = (props: Props) => {
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent
className="Search"
headerref={headerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<div id="Header">
<div className="Search DialogHeader" ref={headerRef}>
<div id="Bar">
<Input
autoComplete="off"

View file

@ -2,12 +2,11 @@
gap: $unit;
min-width: $unit * 52;
form {
.Fields {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
margin-bottom: $unit-2x;
padding: 0 $unit-3x;
padding: 0 $unit-4x;
.terms {
color: $grey-50;

View file

@ -9,7 +9,7 @@ import setUserToken from '~utils/setUserToken'
import { accountState } from '~utils/accountState'
import Button from '~components/Button'
import Input from '~components/LabelledInput'
import Input from '~components/Input'
import { Dialog, DialogTrigger, DialogClose } from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import CrossIcon from '~public/icons/Cross.svg'
@ -49,6 +49,8 @@ const SignupModal = (props: Props) => {
const emailInput = React.createRef<HTMLInputElement>()
const passwordInput = React.createRef<HTMLInputElement>()
const passwordConfirmationInput = React.createRef<HTMLInputElement>()
const footerRef = React.createRef<HTMLDivElement>()
const form = [
usernameInput,
emailInput,
@ -279,6 +281,7 @@ const SignupModal = (props: Props) => {
</DialogTrigger>
<DialogContent
className="Signup"
footerref={footerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
@ -292,48 +295,54 @@ const SignupModal = (props: Props) => {
</div>
<form className="form" onSubmit={register}>
<Input
className="Bound"
name="username"
placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange}
error={errors.username}
ref={usernameInput}
/>
<div className="Fields">
<Input
className="Bound"
name="username"
placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange}
error={errors.username}
ref={usernameInput}
/>
<Input
className="Bound"
name="email"
placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange}
error={errors.email}
ref={emailInput}
/>
<Input
className="Bound"
name="email"
placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange}
error={errors.email}
ref={emailInput}
/>
<Input
className="Bound"
name="password"
placeholder={t('modals.signup.placeholders.password')}
type="password"
onChange={handlePasswordChange}
error={errors.password}
ref={passwordInput}
/>
<Input
className="Bound"
name="password"
placeholder={t('modals.signup.placeholders.password')}
type="password"
onChange={handlePasswordChange}
error={errors.password}
ref={passwordInput}
/>
<Input
className="Bound"
name="confirm_password"
placeholder={t('modals.signup.placeholders.password_confirm')}
type="password"
onChange={handlePasswordChange}
error={errors.passwordConfirmation}
ref={passwordConfirmationInput}
/>
<Input
className="Bound"
name="confirm_password"
placeholder={t('modals.signup.placeholders.password_confirm')}
type="password"
onChange={handlePasswordChange}
error={errors.passwordConfirmation}
ref={passwordConfirmationInput}
/>
</div>
<Button
disabled={!formValid}
text={t('modals.signup.buttons.confirm')}
/>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<Button
disabled={!formValid}
text={t('modals.signup.buttons.confirm')}
/>
</div>
</div>
<p className="terms">
{/* <Trans i18nKey="modals.signup.agreement">

View file

@ -30,6 +30,9 @@ const WeaponConflictModal = (props: Props) => {
// States
const [open, setOpen] = useState(false)
// Refs
const footerRef = React.createRef<HTMLDivElement>()
useEffect(() => {
setOpen(props.open)
}, [setOpen, props.open])
@ -67,40 +70,50 @@ const WeaponConflictModal = (props: Props) => {
<Dialog open={open} onOpenChange={openChange}>
<DialogContent
className="Conflict"
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close}
>
<p>{infoString()}</p>
<div className="WeaponDiagram Diagram">
<ul>
{props.conflictingWeapons?.map((weapon, i) => (
<li className="weapon" key={`conflict-${i}`}>
<div className="Content">
<p>{infoString()}</p>
<div className="WeaponDiagram Diagram">
<ul>
{props.conflictingWeapons?.map((weapon, i) => (
<li className="weapon" key={`conflict-${i}`}>
<img
alt={weapon.object.name[locale]}
src={imageUrl(weapon.object)}
/>
<span>{weapon.object.name[locale]}</span>
</li>
))}
</ul>
<span className="arrow">&rarr;</span>
<div className="wrapper">
<div className="weapon">
<img
alt={weapon.object.name[locale]}
src={imageUrl(weapon.object)}
alt={props.incomingWeapon?.name[locale]}
src={imageUrl(props.incomingWeapon)}
/>
<span>{weapon.object.name[locale]}</span>
</li>
))}
</ul>
<span className="arrow">&rarr;</span>
<div className="wrapper">
<div className="weapon">
<img
alt={props.incomingWeapon?.name[locale]}
src={imageUrl(props.incomingWeapon)}
/>
{props.incomingWeapon?.name[locale]}
{props.incomingWeapon?.name[locale]}
</div>
</div>
</div>
</div>
<footer>
<Button onClick={close} text={t('buttons.cancel')} />
<Button
onClick={props.resolveConflict}
text={t('modals.conflict.buttons.confirm')}
/>
</footer>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<Button
contained={true}
onClick={close}
text={t('buttons.cancel')}
/>
<Button
contained={true}
onClick={props.resolveConflict}
text={t('modals.conflict.buttons.confirm')}
/>
</div>
</div>
</DialogContent>
<Overlay open={open} visible={true} />
</Dialog>

View file

@ -93,6 +93,10 @@ const WeaponModal = ({
const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Hooks
useEffect(() => {
setOpen(modalOpen)
@ -352,10 +356,12 @@ const WeaponModal = ({
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="Weapon"
headerref={headerRef}
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader Short">
<div className="DialogHeader Short" ref={headerRef}>
<img
alt={gridWeapon.object.name[locale]}
className="DialogImage"
@ -382,7 +388,7 @@ const WeaponModal = ({
{gridWeapon.object.ax ? axSelect() : ''}
{gridWeapon.awakening ? awakeningSelect() : ''}
</div>
<div className="DialogFooter">
<div className="DialogFooter" ref={footerRef}>
<Button
contained={true}
onClick={updateWeapon}

View file

@ -291,12 +291,12 @@ i.tag {
@keyframes openModalDesktop {
0% {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
transform: scale(0.96);
}
100% {
// opacity: 1;
transform: translate(-50%, -50%) scale(1);
transform: scale(1);
}
}
@ -311,3 +311,13 @@ i.tag {
transform: translate(0, 30%);
}
}
@keyframes fadeInFilter {
from {
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
}
to {
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(1);
}
}

View file

@ -315,5 +315,6 @@ $hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
$hover-shadow: rgba(0, 0, 0, 0.08) 0px 0px 14px;
// Durations
$duration-modal-open: 0.48s;
$duration-color-fade: 0.24s;
$duration-zoom: 0.18s;