diff --git a/src/assets/alveus-logo.png b/src/assets/alveus.png similarity index 100% rename from src/assets/alveus-logo.png rename to src/assets/alveus.png diff --git a/src/assets/arrow.png b/src/assets/arrow.png deleted file mode 100644 index 3fab5cd6..00000000 Binary files a/src/assets/arrow.png and /dev/null differ diff --git a/src/assets/mod.png b/src/assets/mod.png deleted file mode 100644 index 7e38372e..00000000 Binary files a/src/assets/mod.png and /dev/null differ diff --git a/src/assets/mod.svg b/src/assets/mod.svg new file mode 100644 index 00000000..bba2d95d --- /dev/null +++ b/src/assets/mod.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/overlay/ambassadors.png b/src/assets/overlay/ambassadors.png deleted file mode 100644 index abe443d2..00000000 Binary files a/src/assets/overlay/ambassadors.png and /dev/null differ diff --git a/src/assets/overlay/settings.png b/src/assets/overlay/settings.png deleted file mode 100644 index 4a710352..00000000 Binary files a/src/assets/overlay/settings.png and /dev/null differ diff --git a/src/assets/overlay/welcome.png b/src/assets/overlay/welcome.png deleted file mode 100644 index 3a8c5d8c..00000000 Binary files a/src/assets/overlay/welcome.png and /dev/null differ diff --git a/src/assets/partyHat.png b/src/assets/party.png similarity index 100% rename from src/assets/partyHat.png rename to src/assets/party.png diff --git a/src/components/ambassadorButton/ambassadorButton.module.scss b/src/components/ambassadorButton/ambassadorButton.module.scss index 94667915..47e0af13 100644 --- a/src/components/ambassadorButton/ambassadorButton.module.scss +++ b/src/components/ambassadorButton/ambassadorButton.module.scss @@ -5,7 +5,6 @@ flex-direction: column; justify-content: flex-start; align-items: center; - padding: 1rem 0.85rem; background-color: $primary-color; text-align: center; @@ -25,8 +24,7 @@ } .img { - margin-bottom: 0.25rem; - border-radius: 0.25rem; + border-radius: 0.5rem 0.5rem 0 0; flex-shrink: 0; // prevent long names/species to shrink the image height // crop image to 2.2:1: width: 100%; @@ -36,16 +34,17 @@ .info { margin: auto 0; - } - - .name { - color: $primary-text; - font-size: 0.8rem; - } - - .species { - font-size: 0.7rem; - color: $secondary-text; - line-height: 1.2; // slightly reduce line height if species is multi-line + padding: 0.4rem 0.25rem 0.6rem; + + .name { + color: $primary-text; + font-size: 0.8rem; + } + + .species { + font-size: 0.7rem; + color: $secondary-text; + line-height: 1.2; // slightly reduce line height if species is multi-line + } } } diff --git a/src/components/ambassadorCard/AmbassadorCard.tsx b/src/components/ambassadorCard/AmbassadorCard.tsx index 94f2a565..33ce0eea 100644 --- a/src/components/ambassadorCard/AmbassadorCard.tsx +++ b/src/components/ambassadorCard/AmbassadorCard.tsx @@ -5,18 +5,26 @@ import { getIUCNStatus, type AmbassadorKey, type Ambassador as AmbassadorType, + type AmbassadorImage, } from "../../utils/ambassadors"; import { camelToKebab } from "../../utils/helpers"; import { classes } from "../../utils/classes"; import { normalizeAmbassadorName } from "../../hooks/useChatCommand"; -import moderatorBadge from "../../assets/mod.png"; +import IconInfo from "../icons/IconInfo"; import Tooltip from "../tooltip/Tooltip"; +import moderatorBadge from "../../assets/mod.svg"; + import styles from "./ambassadorCard.module.scss"; +const offsetPosition = (position: AmbassadorImage["position"]) => { + const [x, y] = (position || "50% 50%").split(" "); + return `${x} min(calc(${y} + 1.5rem), 0%)`; +}; + export interface AmbassadorCardProps { ambassadorKey: AmbassadorKey; ambassador: AmbassadorType; @@ -39,25 +47,30 @@ export default function AmbassadorCard(props: AmbassadorCardProps) { ambassador.birth && isBirthday(ambassador.birth) && styles.birthday, )} > - {props.onClose && ( -
- × -
- )} +
+ {images[0].alt} + +
+ {props.onClose && ( +
+ × +
+ )} -

- {ambassador.name} -

- {images[0].alt} +

+ {ambassador.name} +

+
+
{mod && ( -
+
Moderator badge

Show this card to everyone by using{" "} @@ -67,7 +80,7 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {

)} -
+

Species

{ambassador.species}

@@ -77,7 +90,7 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {

-
+

Sex

{ambassador.sex || "Unknown"}

@@ -94,17 +107,17 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {
-
+

Story

{ambassador.story}

-
+

Conservation Mission

{ambassador.mission}

-
+

Conservation Status

- - {/* svg sourced from https://icons.getbootstrap.com/icons/info-circle-fill/ */} - - - +

IUCN: {getIUCNStatus(ambassador.iucn.status)}

-
+

Native To

{ambassador.native.text}

-
+

Learn more about {ambassador.name} on the{" "} + + + ); +} diff --git a/src/components/icons/IconCheck.tsx b/src/components/icons/IconCheck.tsx new file mode 100644 index 00000000..1c9197ab --- /dev/null +++ b/src/components/icons/IconCheck.tsx @@ -0,0 +1,14 @@ +import { BaseIcon, type IconProps } from "./BaseIcon"; + +// This SVG code is derived from Heroicons (https://heroicons.com) +// check +export default function IconCheck(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/icons/IconChevron.tsx b/src/components/icons/IconChevron.tsx new file mode 100644 index 00000000..c7bf2add --- /dev/null +++ b/src/components/icons/IconChevron.tsx @@ -0,0 +1,14 @@ +import { BaseIcon, type IconProps } from "./BaseIcon"; + +// This SVG code is derived from Heroicons (https://heroicons.com) +// chevron-up +export default function IconChevron(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/icons/IconInfo.tsx b/src/components/icons/IconInfo.tsx new file mode 100644 index 00000000..36b371a2 --- /dev/null +++ b/src/components/icons/IconInfo.tsx @@ -0,0 +1,14 @@ +import { BaseIcon, type IconProps } from "./BaseIcon"; + +// This SVG code is derived from Heroicons (https://heroicons.com) +// information-circle +export default function IconInfo(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/icons/IconSettings.tsx b/src/components/icons/IconSettings.tsx new file mode 100644 index 00000000..00f3b4a7 --- /dev/null +++ b/src/components/icons/IconSettings.tsx @@ -0,0 +1,15 @@ +import { BaseIcon, type IconProps } from "./BaseIcon"; + +// This SVG code is derived from Heroicons (https://heroicons.com) +// cog-6-tooth-solid +export default function IconSettings(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/icons/IconWelcome.tsx b/src/components/icons/IconWelcome.tsx new file mode 100644 index 00000000..be6f1b86 --- /dev/null +++ b/src/components/icons/IconWelcome.tsx @@ -0,0 +1,25 @@ +import { BaseIcon, type IconProps } from "./BaseIcon"; + +// This SVG code is derived from an icon @MattIPv4 drew +export default function IconWelcome(props: IconProps) { + return ( + + + + + + + ); +} diff --git a/src/pages/overlay/components/buttons/Buttons.tsx b/src/pages/overlay/components/buttons/Buttons.tsx index 3e2f7901..15b64ce1 100644 --- a/src/pages/overlay/components/buttons/Buttons.tsx +++ b/src/pages/overlay/components/buttons/Buttons.tsx @@ -9,10 +9,9 @@ import styles from "./buttons.module.scss"; type ButtonsOptions = Readonly< { key: string; - title: string; type: "primary" | "secondary"; - icon: string; - hoverText: string; + icon: (props: { size: number }) => JSX.Element; + title: string; }[] >; @@ -47,7 +46,7 @@ export default function Buttons( return (

{optionsWithOnClick.map((option) => ( - + ))} diff --git a/src/pages/overlay/components/buttons/buttons.module.scss b/src/pages/overlay/components/buttons/buttons.module.scss index 2938244d..4e182a5e 100644 --- a/src/pages/overlay/components/buttons/buttons.module.scss +++ b/src/pages/overlay/components/buttons/buttons.module.scss @@ -15,16 +15,13 @@ align-items: center; border-radius: 0.5rem; background: $primary-color; + color: $primary-text; cursor: pointer; box-shadow: $shadow; outline-color: $outline-color; transition: $transition; transition-property: outline, filter; - img { - height: 60%; - } - &:hover, &.highlighted { outline: $outline; diff --git a/src/pages/overlay/components/overlay/Overlay.tsx b/src/pages/overlay/components/overlay/Overlay.tsx index 920b1083..c99724aa 100644 --- a/src/pages/overlay/components/overlay/Overlay.tsx +++ b/src/pages/overlay/components/overlay/Overlay.tsx @@ -8,6 +8,10 @@ import { type Dispatch, } from "react"; +import IconWelcome from "../../../../components/icons/IconWelcome"; +import IconAmbassadors from "../../../../components/icons/IconAmbassadors"; +import IconSettings from "../../../../components/icons/IconSettings"; + import { isAmbassadorKey, type AmbassadorKey, @@ -19,13 +23,8 @@ import useChatCommand from "../../../../hooks/useChatCommand"; import useSettings from "../../hooks/useSettings"; import useSleeping from "../../hooks/useSleeping"; -import WelcomeIcon from "../../../../assets/overlay/welcome.png"; import WelcomeOverlay from "./welcome/Welcome"; - -import AmbassadorsIcon from "../../../../assets/overlay/ambassadors.png"; import AmbassadorsOverlay from "./ambassadors/Ambassadors"; - -import SettingsIcon from "../../../../assets/overlay/settings.png"; import SettingsOverlay from "./settings/Settings"; import Buttons from "../buttons/Buttons"; @@ -38,26 +37,23 @@ const commandTimeout = 10_000; const overlayOptions = [ { key: "welcome", - title: "Welcome", type: "primary", - icon: WelcomeIcon, - hoverText: "Welcome to Alveus", + icon: IconWelcome, + title: "Welcome to Alveus", component: WelcomeOverlay, }, { key: "ambassadors", - title: "Ambassadors", type: "primary", - icon: AmbassadorsIcon, - hoverText: "Explore our Ambassadors", + icon: IconAmbassadors, + title: "Explore our Ambassadors", component: AmbassadorsOverlay, }, { key: "settings", - title: "Settings", type: "secondary", - icon: SettingsIcon, - hoverText: "Extension Settings", + icon: IconSettings, + title: "Extension Settings", component: SettingsOverlay, }, ] as const; diff --git a/src/pages/overlay/components/overlay/ambassadors/Ambassadors.tsx b/src/pages/overlay/components/overlay/ambassadors/Ambassadors.tsx index bf17789b..bbb37335 100644 --- a/src/pages/overlay/components/overlay/ambassadors/Ambassadors.tsx +++ b/src/pages/overlay/components/overlay/ambassadors/Ambassadors.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useCallback } from "react"; +import { useRef, useEffect, useCallback, type MouseEvent } from "react"; import AmbassadorCard from "../../../../../components/ambassadorCard/AmbassadorCard"; import AmbassadorButton from "../../../../../components/ambassadorButton/AmbassadorButton"; @@ -9,11 +9,10 @@ import { } from "../../../../../utils/ambassadors"; import { classes } from "../../../../../utils/classes"; -import arrow from "../../../../../assets/arrow.png"; - import type { OverlayOptionProps } from "../Overlay"; import styles from "./ambassadors.module.scss"; +import IconChevron from "../../../../../components/icons/IconChevron"; export default function Ambassadors(props: OverlayOptionProps) { const { @@ -46,44 +45,48 @@ export default function Ambassadors(props: OverlayOptionProps) { }, [activeAmbassador]); // Allow the list to be scrolled via the buttons - const ambassadorListScroll = useCallback((direction: number) => { - if (ambassadorList.current) - ambassadorList.current.scroll({ - top: ambassadorList.current.scrollTop - direction, - left: 0, - behavior: "smooth", - }); - }, []); + const ambassadorListScroll = useCallback( + (event: MouseEvent, direction: number) => { + if (ambassadorList.current) { + event.stopPropagation(); + + ambassadorList.current.scroll({ + top: ambassadorList.current.scrollTop - direction, + left: 0, + behavior: "smooth", + }); + } + }, + [], + ); // Ensure the buttons are only shown if the list is scrollable const handleArrowVisibility = useCallback(() => { if (ambassadorList.current) { if (ambassadorList.current.scrollTop === 0) - upArrowRef.current?.classList.add(styles.arrowHidden); + upArrowRef.current?.classList.add(styles.hidden); else if ( ambassadorList.current.scrollTop + ambassadorList.current.clientHeight === ambassadorList.current.scrollHeight ) - downArrowRef.current?.classList.add(styles.arrowHidden); + downArrowRef.current?.classList.add(styles.hidden); else { - upArrowRef.current?.classList.remove(styles.arrowHidden); - downArrowRef.current?.classList.remove(styles.arrowHidden); + upArrowRef.current?.classList.remove(styles.hidden); + downArrowRef.current?.classList.remove(styles.hidden); } } }, []); + // Check the arrow visibility on mount + // Sometimes browsers restore odd scroll positions + useEffect(() => { + handleArrowVisibility(); + }, [handleArrowVisibility]); + return (
-
- - +
+ +
diff --git a/src/pages/overlay/components/overlay/ambassadors/ambassadors.module.scss b/src/pages/overlay/components/overlay/ambassadors/ambassadors.module.scss index 7e5ca820..46eac107 100644 --- a/src/pages/overlay/components/overlay/ambassadors/ambassadors.module.scss +++ b/src/pages/overlay/components/overlay/ambassadors/ambassadors.module.scss @@ -1,5 +1,8 @@ @import "../../../../../variables"; +$container-overflow: $twitch-vertical-padding; +$fade-distance: 3rem; + .ambassadorList { position: absolute; top: 0; @@ -8,7 +11,7 @@ display: flex; z-index: 0; // lets tooltips appear above the list - .scrollAmbassadors { + .scroll { display: flex; flex-direction: column; align-items: center; @@ -21,20 +24,24 @@ align-items: center; width: 10rem; - padding: 1.5rem 1rem; + margin: -$container-overflow 0; + padding: ($container-overflow + $fade-distance) 1rem; gap: 1rem; // mask image to fade out the list to transparent top and bottom // using a gradient as image of which the alpha channel will be // applied to the content of the list - // 0% - 3% gradient from 0% to 100% alpha - // 3% - 97% 100% alpha - // 97% - 100% gradient from 100% to 0% alpha mask-image: linear-gradient( to bottom, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 1) 3%, - rgba(0, 0, 0, 1) 97%, + /* gradient from 0% to 100% alpha, + with most of the transition between the overflow and fade distance */ + rgba(0, 0, 0, 0) 0, + rgba(0, 0, 0, 0.25) #{$container-overflow}, + rgba(0, 0, 0, 1) #{$container-overflow + $fade-distance}, + /* gradient from 100% to 0% alpha, + with most of the transition between the overflow and fade distance */ + rgba(0, 0, 0, 1) calc(100% - #{$container-overflow + $fade-distance}), + rgba(0, 0, 0, 0.25) calc(100% - #{$container-overflow}), rgba(0, 0, 0, 0) 100% ); @@ -47,42 +54,88 @@ } } + &::before, + &::after { + content: ""; + display: block; + position: absolute; + width: 100%; + + height: #{$container-overflow + $fade-distance}; + mask-image: linear-gradient( + to bottom, + /* gradient from 100% to 0% alpha/blur, + with the transition happening after the overflow */ + rgba(0, 0, 0, 1) 0, + rgba(0, 0, 0, 1) #{$container-overflow}, + rgba(0, 0, 0, 0) 100% + ); + backdrop-filter: blur(0.125rem); + + z-index: 1; + } + + &::before { + top: -$container-overflow; + } + + &::after { + bottom: -$container-overflow; + transform: rotate(180deg); + } + .arrow { position: absolute; border: 0; cursor: pointer; - width: 2.5rem; - height: 2.5rem; - padding: 0.5rem; - border-radius: 1.25rem; - background-clip: content-box; - background: rgba(0, 0, 0, 0.3); - backdrop-filter: blur(0.25rem); + color: $primary-color; + width: 100%; + height: $fade-distance; + padding: 0 0 1rem; // offset toward the faded edge + background: rgba( + $accent-color, + 0.01% + ); // non-default "transparent" background to avoid dismissing z-index: 2; - transition: 0.3s ease; - transition-property: scale, visibility, opacity; + transition: $transition opacity; &:hover { - scale: 1.4; + svg { + scale: 1.2; + + path { + stroke: $outline-color; + stroke-width: 0.375rem; + } + } } - &Up { + &.up { top: 0; } - &Down { + &.down { bottom: 0; transform: rotate(180deg); } - &Hidden { - visibility: hidden; + &.hidden { opacity: 0; + pointer-events: none; } - img { - width: 100%; - height: auto; + svg { + filter: drop-shadow(0 0 0.5rem rgba($accent-color, 0.75)); + overflow: visible; // ensure stroke doesn't get clipped + transition: $transition scale; + + path { + stroke: $secondary-color; + stroke-width: 0.25rem; + paint-order: stroke; // force the stroke to be under the fill + transition: $transition; + transition-property: stroke-width, stroke; + } } } } diff --git a/src/pages/overlay/components/toggle/Toggle.tsx b/src/pages/overlay/components/toggle/Toggle.tsx index 3646cc3b..16e48102 100644 --- a/src/pages/overlay/components/toggle/Toggle.tsx +++ b/src/pages/overlay/components/toggle/Toggle.tsx @@ -1,5 +1,7 @@ import { useCallback, type ChangeEvent } from "react"; +import IconCheck from "../../../../components/icons/IconCheck"; + import styles from "./toggle.module.scss"; interface ToggleProps { @@ -22,7 +24,7 @@ export default function Toggle(props: ToggleProps) { diff --git a/src/pages/overlay/components/toggle/toggle.module.scss b/src/pages/overlay/components/toggle/toggle.module.scss index 84eac148..5ac70889 100644 --- a/src/pages/overlay/components/toggle/toggle.module.scss +++ b/src/pages/overlay/components/toggle/toggle.module.scss @@ -32,13 +32,13 @@ &:checked { background: $secondary-color; - ~ span { + ~ svg { opacity: 1; } } } - span { + svg { color: $primary-color; opacity: 0; position: absolute; diff --git a/src/pages/overlay/index.scss b/src/pages/overlay/index.scss index 92722f8b..32d7c159 100644 --- a/src/pages/overlay/index.scss +++ b/src/pages/overlay/index.scss @@ -25,7 +25,8 @@ body { width: 100vw; height: 100vh; overflow: hidden; - padding: 5rem 7rem 5rem 0; // https://dev.twitch.tv/docs/extensions/designing/#video-overlay-extensions + padding: $twitch-vertical-padding $twitch-right-padding + $twitch-vertical-padding $twitch-left-padding; #root { width: 100%; diff --git a/src/pages/panel/components/nav/Nav.tsx b/src/pages/panel/components/nav/Nav.tsx index d7f9034d..1d1ebcd8 100644 --- a/src/pages/panel/components/nav/Nav.tsx +++ b/src/pages/panel/components/nav/Nav.tsx @@ -1,11 +1,11 @@ -import AlveusLogo from "../../../../assets/alveus-logo.png"; +import alveus from "../../../../assets/alveus.png"; import styles from "./nav.module.scss"; export default function Nav() { return ( ); diff --git a/src/utils/ambassadors.ts b/src/utils/ambassadors.ts index 1df9c56a..6dc3be67 100644 --- a/src/utils/ambassadors.ts +++ b/src/utils/ambassadors.ts @@ -8,7 +8,10 @@ import { type ActiveAmbassadorKey as AmbassadorKey, } from "@alveusgg/data/src/ambassadors/filters"; import { getClassification } from "@alveusgg/data/src/ambassadors/classification"; -import { getAmbassadorImages } from "@alveusgg/data/src/ambassadors/images"; +import { + getAmbassadorImages, + type AmbassadorImage, +} from "@alveusgg/data/src/ambassadors/images"; import { getIUCNStatus } from "@alveusgg/data/src/iucn"; import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "./helpers"; @@ -28,4 +31,5 @@ export { type Ambassadors, type AmbassadorKey, type Ambassador, + type AmbassadorImage, }; diff --git a/src/variables.scss b/src/variables.scss index 02d1d850..031f4087 100644 --- a/src/variables.scss +++ b/src/variables.scss @@ -1,4 +1,4 @@ -@import "https://fonts.googleapis.com/css2?family=Nunito:wght@400;500&display=swap"; +@import "https://fonts.googleapis.com/css2?family=Nunito:wght@300;500;700&display=swap"; $primary-color: #636a60; $secondary-color: #c2b3a7; @@ -7,6 +7,7 @@ $accent-color: #272b27; $primary-text: white; $secondary-text: #dcdcdc; +$heading-text: lighten($primary-color, 25%); $outline-color: #ff9f1c; $tooltip-background-color: #18181b; @@ -20,14 +21,7 @@ $transition: 0.3s ease; $slide-distance: 2.5rem; -// used to scale various elements based on available height -// but clamped between 6px and 10px to prevent tiny/giant UI -// the values are chosen so that in a normal desktop resolution -// 1 base unit === 10px to make the math easier when using it -// to scale elements. -// -// usage example: -// width: calc(5 * $overlay-base-size); -// => which would be 50px in normal views and -// would scale down to 30px -$overlay-base-size: clamp(6px, 1.46vh, 10px); +// https://dev.twitch.tv/docs/extensions/designing/#video-overlay-extensions +$twitch-vertical-padding: 5rem; +$twitch-right-padding: 7rem; +$twitch-left-padding: 0rem;