feat(app): banner, style tweaks
This commit is contained in:
@@ -41,7 +41,6 @@
|
|||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"recoil": "^0.7.6",
|
"recoil": "^0.7.6",
|
||||||
"svg2png-converter": "^1.0.2",
|
"svg2png-converter": "^1.0.2",
|
||||||
"tinycolor": "^0.0.1",
|
|
||||||
"tinycolor2": "^1.4.2"
|
"tinycolor2": "^1.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
:root {
|
:root {
|
||||||
--red: #ff6e60;
|
--red: #ff6e60;
|
||||||
--blue: #397fff;
|
--orange: #ff8e51;
|
||||||
--yellow: #ffd171;
|
--yellow: #ffd171;
|
||||||
|
--pale: #ffe8dc;
|
||||||
|
--peach: #ffd5c0;
|
||||||
|
--darkgreen: #245633;
|
||||||
|
--blue: #397fff;
|
||||||
--purple: #925bff;
|
--purple: #925bff;
|
||||||
--eggplant: #35313d;
|
--eggplant: #35313d;
|
||||||
--neutral: #86838b;
|
--neutral: #86838b;
|
||||||
@@ -59,6 +63,7 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.main-button {
|
button.main-button {
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
.banner {
|
.banner {
|
||||||
display: grid;
|
position: fixed;
|
||||||
place-items: center;
|
top: 0;
|
||||||
padding: 24px;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
/* top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
max-width: 1120px; */
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
background-color: var(--eggplant);
|
background-color: var(--eggplant);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner .main-button {
|
.banner .main-button {
|
||||||
@@ -12,3 +22,31 @@
|
|||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-button {
|
||||||
|
color: inherit;
|
||||||
|
background: var(--eggplant);
|
||||||
|
height: unset !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 48px !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-button:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,80 @@
|
|||||||
import { Medal } from "phosphor-react";
|
import { ReactNode, Dispatch, SetStateAction } from "react";
|
||||||
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import { XCircle } from "phosphor-react";
|
||||||
import ReactGA from "react-ga4";
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
|
import { useLocalStorage } from "@/hooks";
|
||||||
|
|
||||||
import "./Banner.css";
|
import "./Banner.css";
|
||||||
|
|
||||||
const Banner = () => {
|
type BannerState = {
|
||||||
const handleClick = () => {
|
seen: Record<string, boolean>;
|
||||||
ReactGA.event({ category: "Outbound", action: "Click", label: "Vote" });
|
};
|
||||||
window.open(
|
|
||||||
"https://www.figma.com/community/file/903830135544202908",
|
type BannerProps = {
|
||||||
"_blank",
|
id: string;
|
||||||
"noopener noreferrer"
|
children?: ReactNode;
|
||||||
);
|
onClose?: (dispatch: Dispatch<SetStateAction<BannerState>>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
initial: { y: -120 },
|
||||||
|
animate: { y: 0 },
|
||||||
|
exit: { y: -120 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const BANNER_STATE_KEY = "banner_state";
|
||||||
|
|
||||||
|
const Banner = ({ id, children, onClose }: BannerProps) => {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
seen: { [id]: seen },
|
||||||
|
},
|
||||||
|
setBannerState,
|
||||||
|
] = useLocalStorage<BannerState>(BANNER_STATE_KEY, {
|
||||||
|
seen: { [id]: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
ReactGA.event({
|
||||||
|
category: "Banner",
|
||||||
|
action: "Dismiss",
|
||||||
|
label: id,
|
||||||
|
});
|
||||||
|
onClose
|
||||||
|
? onClose(setBannerState)
|
||||||
|
: setBannerState((state) => ({
|
||||||
|
...state,
|
||||||
|
seen: { ...state.seen, [id]: true },
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="banner">
|
<AnimatePresence initial={true}>
|
||||||
The 2022 Figma Community Awards are here!
|
{!seen && (
|
||||||
<button className="main-button" onClick={handleClick}>
|
<motion.aside
|
||||||
<Medal size={24} weight="fill" />
|
className="card banner"
|
||||||
Vote for Phosphor
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={variants}
|
||||||
|
>
|
||||||
|
<div className="banner-content">
|
||||||
|
{children}
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
className="banner-button"
|
||||||
|
onClick={handleClose}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.key === "Enter" && handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XCircle color="currentColor" size={28} weight="fill" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ footer {
|
|||||||
font-size: 56px;
|
font-size: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#back-to-top-button:active {
|
||||||
|
transform: translate(4px, 4px) !important;
|
||||||
|
box-shadow: 0 0 0 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
#back-to-top-button svg {
|
#back-to-top-button svg {
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
@@ -138,7 +143,7 @@ footer .links {
|
|||||||
display: initial;
|
display: initial;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -240px;
|
left: -240px;
|
||||||
top: 656px;
|
top: 632px;
|
||||||
height: 584px;
|
height: 584px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
header {
|
header {
|
||||||
width: 100%;
|
|
||||||
background-color: var(--yellow);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-contents {
|
.header-contents {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react";
|
import { ArrowCircleUpRight, ArrowCircleDown, Megaphone } from "phosphor-react";
|
||||||
|
|
||||||
|
import Banner from "@/components/Banner";
|
||||||
|
|
||||||
import { ReactComponent as MarkerPurple } from "@/assets/marker-purple.svg";
|
import { ReactComponent as MarkerPurple } from "@/assets/marker-purple.svg";
|
||||||
import { ReactComponent as PaperClips } from "@/assets/paperclips-header-mobile.svg";
|
import { ReactComponent as PaperClips } from "@/assets/paperclips-header-mobile.svg";
|
||||||
@@ -36,6 +38,22 @@ const handleScrollToIcons = () =>
|
|||||||
const Header = (_: HeaderProps) => {
|
const Header = (_: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
|
<Banner
|
||||||
|
id={Math.random().toString()}
|
||||||
|
children={
|
||||||
|
<>
|
||||||
|
<Megaphone mirrored color="var(--orange)" size={28} weight="fill" />
|
||||||
|
<small>
|
||||||
|
Phosphor has some big updates, and some APIs have changed for
|
||||||
|
users of the Vanilla JS library. Please check our{" "}
|
||||||
|
<a href="https://github.com/phosphor-icons/homepage#readme">
|
||||||
|
documentation
|
||||||
|
</a>{" "}
|
||||||
|
to see what's new...
|
||||||
|
</small>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className="header-contents">
|
<div className="header-contents">
|
||||||
<div className="illustrations-top">
|
<div className="illustrations-top">
|
||||||
<MarkerPurple id="marker-purple" />
|
<MarkerPurple id="marker-purple" />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Copy, CheckCircle, DownloadSimple, XCircle } from "phosphor-react";
|
|||||||
import ReactGA from "react-ga4";
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
import Tabs, { Tab } from "@/components/Tabs";
|
import Tabs, { Tab } from "@/components/Tabs";
|
||||||
import { useMediaQuery, useTransientState, useSessionState } from "@/hooks";
|
import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks";
|
||||||
import { SnippetType } from "@/lib";
|
import { SnippetType } from "@/lib";
|
||||||
import {
|
import {
|
||||||
iconWeightAtom,
|
iconWeightAtom,
|
||||||
@@ -66,7 +66,7 @@ const DetailFooter = () => {
|
|||||||
);
|
);
|
||||||
const ref = useRef<SVGSVGElement>(null);
|
const ref = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
const [{ i }, setInitialTab] = useSessionState("tab", { i: 0 });
|
const [{ i }, setInitialTab] = useSessionStorage("tab", { i: 0 });
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(max-width: 719px)");
|
const isMobile = useMediaQuery("(max-width: 719px)");
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ const DetailFooter = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={true}>
|
||||||
{!!entry && (
|
{!!entry && (
|
||||||
<motion.aside
|
<motion.aside
|
||||||
initial="initial"
|
initial="initial"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
/* min-height: 80vh; */
|
/* min-height: 80vh; */
|
||||||
|
z-index: 1;
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { useRef, useLayoutEffect, useEffect, MutableRefObject } from "react";
|
import {
|
||||||
|
useRef,
|
||||||
|
useLayoutEffect,
|
||||||
|
useEffect,
|
||||||
|
MutableRefObject,
|
||||||
|
HTMLAttributes,
|
||||||
|
} from "react";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { IconEntry } from "@/lib";
|
import { IconEntry } from "@/lib";
|
||||||
import { selectionEntryAtom } from "@/state";
|
import { selectionEntryAtom } from "@/state";
|
||||||
|
|
||||||
interface IconGridItemProps {
|
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
index: number;
|
index: number;
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
entry: IconEntry;
|
entry: IconEntry;
|
||||||
@@ -25,7 +31,7 @@ const itemVariants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const IconGridItem = (props: IconGridItemProps) => {
|
const IconGridItem = (props: IconGridItemProps) => {
|
||||||
const { index, originOffset, entry } = props;
|
const { index, originOffset, entry, style } = props;
|
||||||
const { name, Icon } = entry;
|
const { name, Icon } = entry;
|
||||||
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||||
const isOpen = selection?.name === name;
|
const isOpen = selection?.name === name;
|
||||||
@@ -68,7 +74,10 @@ const IconGridItem = (props: IconGridItemProps) => {
|
|||||||
key={name}
|
key={name}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={isOpen ? { backgroundColor: "var(--translucent)" } : undefined}
|
style={{
|
||||||
|
...style,
|
||||||
|
backgroundColor: isOpen ? "var(--translucent)" : undefined,
|
||||||
|
}}
|
||||||
custom={delayRef}
|
custom={delayRef}
|
||||||
transition={transition}
|
transition={transition}
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ nav.toolbar {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--eggplant);
|
background-color: var(--eggplant);
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ export { default as useCSSVariables } from "./useCSSVariables";
|
|||||||
export { default as useDebounce } from "./useDebounce";
|
export { default as useDebounce } from "./useDebounce";
|
||||||
export { default as useEvent } from "./useEvent";
|
export { default as useEvent } from "./useEvent";
|
||||||
export { default as useIconParameters } from "./useIconParameters";
|
export { default as useIconParameters } from "./useIconParameters";
|
||||||
|
export { default as useLocalStorage } from "./useLocalStorage";
|
||||||
export { default as useMediaQuery } from "./useMediaQuery";
|
export { default as useMediaQuery } from "./useMediaQuery";
|
||||||
export { default as usePersistSettings } from "./usePersistSettings";
|
export { default as usePersistSettings } from "./usePersistSettings";
|
||||||
export { default as useSessionState } from "./useSessionState";
|
export { default as useSessionStorage } from "./useSessionStorage";
|
||||||
export { default as useThrottle } from "./useThrottle";
|
export { default as useThrottle } from "./useThrottle";
|
||||||
export { default as useThrottled } from "./useThrottled";
|
export { default as useThrottled } from "./useThrottled";
|
||||||
export { default as useTimeoutFn } from "./useTimeoutFn";
|
export { default as useTimeoutFn } from "./useTimeoutFn";
|
||||||
|
|||||||
40
src/hooks/useLocalStorage.ts
Normal file
40
src/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useState, Dispatch, SetStateAction } from "react";
|
||||||
|
import { STORAGE_KEY } from "@/state";
|
||||||
|
|
||||||
|
type Initializer<S> = () => S;
|
||||||
|
type Setter<S> = (prev: S) => S;
|
||||||
|
type Action<S> = S | Setter<S> | Initializer<S>;
|
||||||
|
|
||||||
|
function expand<S extends object>(action: Action<S>, prev?: S) {
|
||||||
|
if (typeof action === "function") {
|
||||||
|
return (action as Setter<S>)(prev!);
|
||||||
|
} else {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useLocalStorage<S extends object>(
|
||||||
|
key: string,
|
||||||
|
fallbackState: S | (() => S)
|
||||||
|
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
|
||||||
|
const [value, setValue] = useState<S>(() => {
|
||||||
|
let val = localStorage.getItem(STORAGE_KEY + key);
|
||||||
|
if (val) return JSON.parse(val) as S;
|
||||||
|
return expand(fallbackState);
|
||||||
|
});
|
||||||
|
|
||||||
|
const set: Dispatch<SetStateAction<S>> = useCallback((val) => {
|
||||||
|
setValue((prev) => {
|
||||||
|
const next = expand(val, prev);
|
||||||
|
localStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
(partial: Partial<S>) => set((value) => ({ ...value, ...partial })),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, set, insert];
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@ function expand<S extends object>(action: Action<S>, prev?: S) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useSessionState<S extends object>(
|
export default function useSessionStorage<S extends object>(
|
||||||
key: string,
|
key: string,
|
||||||
fallbackState: S | (() => S)
|
fallbackState: S | (() => S)
|
||||||
): [S, Dispatch<SetStateAction<S>>] {
|
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
|
||||||
const [value, setValue] = useState<S>(() => {
|
const [value, setValue] = useState<S>(() => {
|
||||||
let val = sessionStorage.getItem(STORAGE_KEY + key);
|
let val = sessionStorage.getItem(STORAGE_KEY + key);
|
||||||
if (val) return JSON.parse(val) as S;
|
if (val) return JSON.parse(val) as S;
|
||||||
@@ -31,5 +31,10 @@ export default function useSessionState<S extends object>(
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return [value, set];
|
const insert = useCallback(
|
||||||
|
(partial: Partial<S>) => set((value) => ({ ...value, ...partial })),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, set, insert];
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,9 @@ export function getCodeSnippets({
|
|||||||
const elmWeight = weight.replace(/^\w/, (c) => c.toUpperCase());
|
const elmWeight = weight.replace(/^\w/, (c) => c.toUpperCase());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[SnippetType.HTML]: `<i class="ph-${name}${
|
[SnippetType.HTML]: `<i class="ph${
|
||||||
isDefaultWeight ? "" : `-${weight}`
|
isDefaultWeight ? "" : `-${weight}`
|
||||||
}"></i>`,
|
} ph-${name}"></i>`,
|
||||||
[SnippetType.REACT]: `<${displayName} size={${size}} ${
|
[SnippetType.REACT]: `<${displayName} size={${size}} ${
|
||||||
!isDefaultColor ? `color="${color}" ` : ""
|
!isDefaultColor ? `color="${color}" ` : ""
|
||||||
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
|
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
|
||||||
@@ -56,6 +56,6 @@ export function supportsWeight({
|
|||||||
type: SnippetType;
|
type: SnippetType;
|
||||||
weight: IconStyle;
|
weight: IconStyle;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
if (type !== SnippetType.HTML && type !== SnippetType.FLUTTER) return true;
|
if (type !== SnippetType.FLUTTER) return true;
|
||||||
return weight !== IconStyle.DUOTONE;
|
return weight !== IconStyle.DUOTONE;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user