feat(app): banner, style tweaks
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
:root {
|
||||
--red: #ff6e60;
|
||||
--blue: #397fff;
|
||||
--orange: #ff8e51;
|
||||
--yellow: #ffd171;
|
||||
--pale: #ffe8dc;
|
||||
--peach: #ffd5c0;
|
||||
--darkgreen: #245633;
|
||||
--blue: #397fff;
|
||||
--purple: #925bff;
|
||||
--eggplant: #35313d;
|
||||
--neutral: #86838b;
|
||||
@@ -59,6 +63,7 @@ button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.main-button {
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
.banner {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
/* top: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
max-width: 1120px; */
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
background-color: var(--eggplant);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.banner .main-button {
|
||||
@@ -12,3 +22,31 @@
|
||||
min-height: 64px;
|
||||
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 { useLocalStorage } from "@/hooks";
|
||||
|
||||
import "./Banner.css";
|
||||
|
||||
const Banner = () => {
|
||||
const handleClick = () => {
|
||||
ReactGA.event({ category: "Outbound", action: "Click", label: "Vote" });
|
||||
window.open(
|
||||
"https://www.figma.com/community/file/903830135544202908",
|
||||
"_blank",
|
||||
"noopener noreferrer"
|
||||
);
|
||||
type BannerState = {
|
||||
seen: Record<string, boolean>;
|
||||
};
|
||||
|
||||
type BannerProps = {
|
||||
id: string;
|
||||
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 (
|
||||
<div className="banner">
|
||||
The 2022 Figma Community Awards are here!
|
||||
<button className="main-button" onClick={handleClick}>
|
||||
<Medal size={24} weight="fill" />
|
||||
Vote for Phosphor
|
||||
</button>
|
||||
</div>
|
||||
<AnimatePresence initial={true}>
|
||||
{!seen && (
|
||||
<motion.aside
|
||||
className="card banner"
|
||||
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>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ footer {
|
||||
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 {
|
||||
margin-right: unset;
|
||||
}
|
||||
@@ -138,7 +143,7 @@ footer .links {
|
||||
display: initial;
|
||||
position: absolute;
|
||||
left: -240px;
|
||||
top: 656px;
|
||||
top: 632px;
|
||||
height: 584px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
header {
|
||||
width: 100%;
|
||||
background-color: var(--yellow);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
|
||||
.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 PaperClips } from "@/assets/paperclips-header-mobile.svg";
|
||||
@@ -36,6 +38,22 @@ const handleScrollToIcons = () =>
|
||||
const Header = (_: HeaderProps) => {
|
||||
return (
|
||||
<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="illustrations-top">
|
||||
<MarkerPurple id="marker-purple" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Copy, CheckCircle, DownloadSimple, XCircle } from "phosphor-react";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import Tabs, { Tab } from "@/components/Tabs";
|
||||
import { useMediaQuery, useTransientState, useSessionState } from "@/hooks";
|
||||
import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks";
|
||||
import { SnippetType } from "@/lib";
|
||||
import {
|
||||
iconWeightAtom,
|
||||
@@ -66,7 +66,7 @@ const DetailFooter = () => {
|
||||
);
|
||||
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)");
|
||||
|
||||
@@ -219,7 +219,7 @@ const DetailFooter = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
<AnimatePresence initial={true}>
|
||||
{!!entry && (
|
||||
<motion.aside
|
||||
initial="initial"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
position: relative;
|
||||
padding: 32px 16px;
|
||||
/* min-height: 80vh; */
|
||||
z-index: 1;
|
||||
content-visibility: auto;
|
||||
color: var(--foreground);
|
||||
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 { motion } from "framer-motion";
|
||||
|
||||
import { IconEntry } from "@/lib";
|
||||
import { selectionEntryAtom } from "@/state";
|
||||
|
||||
interface IconGridItemProps {
|
||||
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
index: number;
|
||||
isDark: boolean;
|
||||
entry: IconEntry;
|
||||
@@ -25,7 +31,7 @@ const itemVariants = {
|
||||
};
|
||||
|
||||
const IconGridItem = (props: IconGridItemProps) => {
|
||||
const { index, originOffset, entry } = props;
|
||||
const { index, originOffset, entry, style } = props;
|
||||
const { name, Icon } = entry;
|
||||
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||
const isOpen = selection?.name === name;
|
||||
@@ -68,7 +74,10 @@ const IconGridItem = (props: IconGridItemProps) => {
|
||||
key={name}
|
||||
ref={ref}
|
||||
tabIndex={0}
|
||||
style={isOpen ? { backgroundColor: "var(--translucent)" } : undefined}
|
||||
style={{
|
||||
...style,
|
||||
backgroundColor: isOpen ? "var(--translucent)" : undefined,
|
||||
}}
|
||||
custom={delayRef}
|
||||
transition={transition}
|
||||
variants={itemVariants}
|
||||
|
||||
@@ -5,7 +5,7 @@ nav.toolbar {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--eggplant);
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,9 +2,10 @@ export { default as useCSSVariables } from "./useCSSVariables";
|
||||
export { default as useDebounce } from "./useDebounce";
|
||||
export { default as useEvent } from "./useEvent";
|
||||
export { default as useIconParameters } from "./useIconParameters";
|
||||
export { default as useLocalStorage } from "./useLocalStorage";
|
||||
export { default as useMediaQuery } from "./useMediaQuery";
|
||||
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 useThrottled } from "./useThrottled";
|
||||
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,
|
||||
fallbackState: S | (() => S)
|
||||
): [S, Dispatch<SetStateAction<S>>] {
|
||||
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
|
||||
const [value, setValue] = useState<S>(() => {
|
||||
let val = sessionStorage.getItem(STORAGE_KEY + key);
|
||||
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());
|
||||
|
||||
return {
|
||||
[SnippetType.HTML]: `<i class="ph-${name}${
|
||||
[SnippetType.HTML]: `<i class="ph${
|
||||
isDefaultWeight ? "" : `-${weight}`
|
||||
}"></i>`,
|
||||
} ph-${name}"></i>`,
|
||||
[SnippetType.REACT]: `<${displayName} size={${size}} ${
|
||||
!isDefaultColor ? `color="${color}" ` : ""
|
||||
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
|
||||
@@ -56,6 +56,6 @@ export function supportsWeight({
|
||||
type: SnippetType;
|
||||
weight: IconStyle;
|
||||
}): boolean {
|
||||
if (type !== SnippetType.HTML && type !== SnippetType.FLUTTER) return true;
|
||||
if (type !== SnippetType.FLUTTER) return true;
|
||||
return weight !== IconStyle.DUOTONE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user