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; gap: 0;
padding-bottom: $unit; padding-bottom: $unit;
& > div:not(.DialogHeader) { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit-2x; gap: $unit-2x;
padding: 0 $unit-4x; padding: 0 $unit-4x;
} }
.sections {
display: flex;
flex-direction: column;
gap: $unit-2x;
}
section { section {
margin-bottom: $unit; margin-bottom: $unit;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,9 @@ const CharacterConflictModal = (props: Props) => {
// States // States
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
// Refs
const footerRef = React.createRef<HTMLDivElement>()
useEffect(() => { useEffect(() => {
setOpen(props.open) setOpen(props.open)
}, [setOpen, props.open]) }, [setOpen, props.open])
@ -73,9 +76,11 @@ const CharacterConflictModal = (props: Props) => {
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<DialogContent <DialogContent
className="Conflict" className="Conflict"
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close} onEscapeKeyDown={close}
> >
<div className="Content">
<p> <p>
<Trans i18nKey="modals.conflict.character"></Trans> <Trans i18nKey="modals.conflict.character"></Trans>
</p> </p>
@ -102,13 +107,21 @@ const CharacterConflictModal = (props: Props) => {
</div> </div>
</div> </div>
</div> </div>
<footer> </div>
<Button onClick={close} text={t('buttons.cancel')} /> <div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<Button <Button
contained={true}
onClick={close}
text={t('buttons.cancel')}
/>
<Button
contained={true}
onClick={props.resolveConflict} onClick={props.resolveConflict}
text={t('modals.conflict.buttons.confirm')} text={t('modals.conflict.buttons.confirm')}
/> />
</footer> </div>
</div>
</DialogContent> </DialogContent>
<Overlay open={open} visible={true} /> <Overlay open={open} visible={true} />
</Dialog> </Dialog>

View file

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

View file

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

View file

@ -1,6 +1,4 @@
.Dialog { .Dialog {
// animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
// openModalDesktop;
position: fixed; position: fixed;
background: none; background: none;
border: 0; border: 0;
@ -16,9 +14,13 @@
.DialogContent { .DialogContent {
$multiplier: 4; $multiplier: 4;
animation: $duration-modal-open cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal
none running openModalDesktop;
background: var(--dialog-bg); background: var(--dialog-bg);
border-radius: $card-corner; border-radius: $card-corner;
box-sizing: border-box; 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; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * $multiplier; gap: $unit * $multiplier;
@ -26,6 +28,7 @@
min-width: $unit * 48; min-width: $unit * 48;
// min-height: $unit-12x; // min-height: $unit-12x;
overflow-y: scroll; overflow-y: scroll;
// height: 80vh;
max-height: 80vh; max-height: 80vh;
min-width: 580px; min-width: 580px;
max-width: 42vw; max-width: 42vw;
@ -49,8 +52,14 @@
width: 100%; width: 100%;
} }
.Scrollable {
overflow-y: auto;
}
.DialogHeader { .DialogHeader {
background: var(--dialog-bg); 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; display: flex;
align-items: center; align-items: center;
gap: $unit-2x; gap: $unit-2x;
@ -137,10 +146,25 @@
align-items: flex-end; align-items: flex-end;
background: var(--dialog-bg); background: var(--dialog-bg);
bottom: 0; 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; display: flex;
flex-direction: column; flex-direction: column;
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x; padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
position: sticky; position: sticky;
.Buttons {
display: flex;
gap: $unit;
&.Span {
width: 100%;
.Button {
width: 100%;
}
}
}
} }
.actions { .actions {
@ -152,9 +176,15 @@
&.Conflict { &.Conflict {
$weapon-diameter: 14rem; $weapon-diameter: 14rem;
.Content {
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: $unit-4x $unit-4x $unit-2x $unit-4x;
& > p { & > p {
line-height: 1.2; font-size: $font-regular;
max-width: 400px; line-height: 1.4;
strong { strong {
font-weight: $bold; font-weight: $bold;
@ -164,6 +194,7 @@
line-height: 1.4; line-height: 1.4;
} }
} }
}
.weapon, .weapon,
.character { .character {
@ -198,7 +229,7 @@
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 2; gap: $unit-2x;
} }
.wrapper { .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 * as DialogPrimitive from '@radix-ui/react-dialog'
import classNames from 'classnames' import classNames from 'classnames'
import debounce from 'lodash.debounce'
import './index.scss'
import Overlay from '~components/Overlay' import Overlay from '~components/Overlay'
import './index.scss'
interface Props interface Props
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>, React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> { > {
headerref?: React.RefObject<HTMLDivElement>
footerref?: React.RefObject<HTMLDivElement>
onEscapeKeyDown: (event: KeyboardEvent) => void onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void onOpenAutoFocus: (event: Event) => void
} }
@ -18,10 +21,106 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
{ children, ...props }, { children, ...props },
forwardedRef forwardedRef
) { ) {
// Classes
const classes = classNames(props.className, { const classes = classNames(props.className, {
DialogContent: true, 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 ( return (
<DialogPrimitive.Portal> <DialogPrimitive.Portal>
<dialog className="Dialog"> <dialog className="Dialog">
@ -32,7 +131,9 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
onEscapeKeyDown={props.onEscapeKeyDown} onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef} ref={forwardedRef}
> >
<div className="Scrollable" onScroll={handleScroll}>
{children} {children}
</div>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</dialog> </dialog>
<Overlay visible={true} open={true} /> <Overlay visible={true} open={true} />

View file

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

View file

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

View file

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

View file

@ -6,6 +6,14 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 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 { &.Visible {
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,9 @@ const WeaponConflictModal = (props: Props) => {
// States // States
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
// Refs
const footerRef = React.createRef<HTMLDivElement>()
useEffect(() => { useEffect(() => {
setOpen(props.open) setOpen(props.open)
}, [setOpen, props.open]) }, [setOpen, props.open])
@ -67,9 +70,11 @@ const WeaponConflictModal = (props: Props) => {
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<DialogContent <DialogContent
className="Conflict" className="Conflict"
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close} onEscapeKeyDown={close}
> >
<div className="Content">
<p>{infoString()}</p> <p>{infoString()}</p>
<div className="WeaponDiagram Diagram"> <div className="WeaponDiagram Diagram">
<ul> <ul>
@ -94,13 +99,21 @@ const WeaponConflictModal = (props: Props) => {
</div> </div>
</div> </div>
</div> </div>
<footer> </div>
<Button onClick={close} text={t('buttons.cancel')} /> <div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<Button <Button
contained={true}
onClick={close}
text={t('buttons.cancel')}
/>
<Button
contained={true}
onClick={props.resolveConflict} onClick={props.resolveConflict}
text={t('modals.conflict.buttons.confirm')} text={t('modals.conflict.buttons.confirm')}
/> />
</footer> </div>
</div>
</DialogContent> </DialogContent>
<Overlay open={open} visible={true} /> <Overlay open={open} visible={true} />
</Dialog> </Dialog>

View file

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

View file

@ -291,12 +291,12 @@ i.tag {
@keyframes openModalDesktop { @keyframes openModalDesktop {
0% { 0% {
opacity: 0; opacity: 0;
transform: translate(-50%, -48%) scale(0.96); transform: scale(0.96);
} }
100% { 100% {
// opacity: 1; // opacity: 1;
transform: translate(-50%, -50%) scale(1); transform: scale(1);
} }
} }
@ -311,3 +311,13 @@ i.tag {
transform: translate(0, 30%); 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; $hover-shadow: rgba(0, 0, 0, 0.08) 0px 0px 14px;
// Durations // Durations
$duration-modal-open: 0.48s;
$duration-color-fade: 0.24s; $duration-color-fade: 0.24s;
$duration-zoom: 0.18s; $duration-zoom: 0.18s;