feat(app): new details footer appearance

This commit is contained in:
rektdeckard
2023-02-05 23:09:20 -07:00
parent 3756374140
commit eba876b3ea
19 changed files with 220 additions and 95 deletions

View File

@@ -31,9 +31,9 @@ pre {
box-sizing: border-box; box-sizing: border-box;
padding: 20px 16px 20px 24px; padding: 20px 16px 20px 24px;
margin: 12px 0px; margin: 12px 0px;
background-color: white; /* background-color: white; */
border-radius: 6px; border-radius: 6px;
border: 1px solid #e1d4d7; /* border: 1px solid #e1d4d7; */
white-space: pre-wrap; white-space: pre-wrap;
} }
@@ -95,17 +95,6 @@ button.main-button svg {
/* gap: 24px; */ /* gap: 24px; */
} }
figure {
margin: 0;
display: grid;
place-items: center;
}
figcaption {
font-size: 14px;
text-align: center;
}
a.main-link { a.main-link {
text-decoration: none; text-decoration: none;
position: relative; position: relative;

View File

@@ -1,9 +1,8 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useRecoilState, useRecoilValue } from "recoil"; import { useRecoilState, useRecoilValue } from "recoil";
import { iconColorAtom } from "@/state/atoms"; import { useThrottled } from "@/hooks";
import { isDarkThemeSelector } from "@/state/selectors"; import { iconColorAtom, isDarkThemeSelector } from "@/state";
import useThrottled from "@/hooks/useThrottled";
import "./ColorInput.css"; import "./ColorInput.css";

View File

@@ -67,7 +67,7 @@ footer .links {
padding: 0; padding: 0;
} }
#back-to-top-button img { #back-to-top-button svg {
width: 28px; width: 28px;
height: 28px; height: 28px;
} }

View File

@@ -4,27 +4,27 @@ import { useHotkeys } from "react-hotkeys-hook";
import { motion, AnimatePresence, Variants } from "framer-motion"; import { motion, AnimatePresence, Variants } from "framer-motion";
import { Svg2Png } from "svg2png-converter"; import { Svg2Png } from "svg2png-converter";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { Copy, CheckCircle, Download } from "phosphor-react"; import { Copy, CheckCircle, DownloadSimple } from "phosphor-react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import Tabs, { Tab } from "@/components/Tabs";
import { useTransientState } from "@/hooks";
import { SnippetType } from "@/lib";
import { import {
iconWeightAtom, iconWeightAtom,
iconSizeAtom, iconSizeAtom,
iconColorAtom, iconColorAtom,
selectionEntryAtom, selectionEntryAtom,
} from "@/state/atoms"; isDarkThemeSelector,
import { isDarkThemeSelector } from "@/state/selectors"; } from "@/state";
import Tabs, { Tab } from "@/components/Tabs";
import useTransientState from "@/hooks/useTransientState";
import { SnippetType } from "@/lib";
import { getCodeSnippets, supportsWeight } from "@/utils"; import { getCodeSnippets, supportsWeight } from "@/utils";
import TagCloud from "./TagCloud"; import TagCloud from "./TagCloud";
const variants: Variants = { const variants: Variants = {
initial: { opacity: 0 }, initial: { y: 188 },
animate: { opacity: 1 }, animate: { y: 0 },
exit: { opacity: 0 }, exit: { y: 188 },
}; };
const RENDERED_SNIPPETS = [ const RENDERED_SNIPPETS = [
@@ -94,10 +94,7 @@ const DetailFooter = () => {
header: type, header: type,
content: ( content: (
<div className="snippet" key={type}> <div className="snippet" key={type}>
<pre <pre style={isWeightSupported ? undefined : snippetButtonStyle}>
tabIndex={0}
style={isWeightSupported ? undefined : snippetButtonStyle}
>
<span> <span>
{isWeightSupported {isWeightSupported
? snippets[type] ? snippets[type]
@@ -213,31 +210,44 @@ const DetailFooter = () => {
<div className="detail-preview"> <div className="detail-preview">
<figure> <figure>
<entry.Icon ref={ref} size={64}></entry.Icon> <entry.Icon ref={ref} size={64}></entry.Icon>
<figcaption>{entry.name}</figcaption> <figcaption>
</figure> <p>{entry.name}</p>
<small className="versioning"> <small className="versioning">
in &ge; {entry.published_in.toFixed(1)}.0 available in v{entry.published_in.toFixed(1)}.0+
</small> </small>
</div> </figcaption>
<Tabs tabs={tabs} /> </figure>
<div className="detail-actions"> <div className="detail-actions">
<button style={buttonBarStyle} onClick={handleDownloadPNG}> <button
<Download size={24} color="currentColor" weight="fill" /> Download tabIndex={0}
PNG style={buttonBarStyle}
onClick={handleDownloadPNG}
>
<DownloadSimple size={24} color="currentColor" weight="fill" /> PNG
</button> </button>
<button style={buttonBarStyle} onClick={handleDownloadSVG}> <button
<Download size={24} color="currentColor" weight="fill" /> Download tabIndex={0}
SVG style={buttonBarStyle}
onClick={handleDownloadSVG}
>
<DownloadSimple size={24} color="currentColor" weight="fill" /> SVG
</button> </button>
<button style={buttonBarStyle} onClick={handleCopySVG}> <button
tabIndex={0}
style={buttonBarStyle}
onClick={handleCopySVG}
>
{copied === "SVG" ? ( {copied === "SVG" ? (
<CheckCircle size={24} color={successColor} weight="fill" /> <CheckCircle size={24} color={successColor} weight="fill" />
) : ( ) : (
<Copy size={24} color="currentColor" weight="fill" /> <Copy size={24} color="currentColor" weight="fill" />
)} )}
{copied === "SVG" ? "Copied!" : "Copy SVG"} {copied === "SVG" ? "Copied!" : " SVG"}
</button> </button>
</div> </div>
</div>
<Tabs tabs={tabs} />
</motion.aside> </motion.aside>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@@ -7,14 +7,14 @@ import { saveAs } from "file-saver";
import { Copy, X, CheckCircle, Download } from "phosphor-react"; import { Copy, X, CheckCircle, Download } from "phosphor-react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { useTransientState } from "@/hooks";
import { IconEntry, SnippetType } from "@/lib";
import { import {
iconWeightAtom, iconWeightAtom,
iconSizeAtom, iconSizeAtom,
iconColorAtom, iconColorAtom,
iconPreviewOpenAtom, iconPreviewOpenAtom,
} from "@/state/atoms"; } from "@/state";
import useTransientState from "@/hooks/useTransientState";
import { IconEntry, SnippetType } from "@/lib";
import { getCodeSnippets, supportsWeight } from "@/utils"; import { getCodeSnippets, supportsWeight } from "@/utils";
import TagCloud from "./TagCloud"; import TagCloud from "./TagCloud";

View File

@@ -119,14 +119,15 @@
} }
.snippet { .snippet {
margin-bottom: 24px; /* margin-bottom: 24px; */
width: 100%;
} }
.snippet pre { .snippet pre {
display: flex; display: flex;
align-items: center; align-items: center;
text-overflow: ellipsis; text-overflow: ellipsis;
color: black; /* color: black; */
-moz-user-select: all; -moz-user-select: all;
-webkit-user-select: all; -webkit-user-select: all;
user-select: all; user-select: all;
@@ -218,16 +219,40 @@ aside.detail-footer {
margin: auto; margin: auto;
max-width: 1120px; max-width: 1120px;
display: grid; display: grid;
grid-template-columns: 144px 1fr 160px; grid-template-columns: 232px 1fr;
gap: 24px;
padding: 12px 24px;
height: 136px;
}
figure {
margin: 0;
display: grid;
grid-template-columns: 64px 1fr;
gap: 24px;
align-items: center;
}
figcaption {
display: flex;
flex-direction: column;
font-size: 14px;
}
figcaption > p {
margin: 0;
} }
.detail-preview { .detail-preview {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; justify-content: center;
padding: 16px; gap: 24px;
} }
.detail-actions { .detail-actions {
padding: 16px; display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
} }

View File

@@ -1,13 +1,15 @@
import { useRef, useEffect } from "react"; import { useRef, useEffect, CSSProperties } from "react";
import { useRecoilValue } from "recoil"; import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion"; import { motion, useAnimation } from "framer-motion";
import { IconContext } from "phosphor-react"; import { IconContext } from "phosphor-react";
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms";
import { import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
filteredQueryResultsSelector, filteredQueryResultsSelector,
isDarkThemeSelector, isDarkThemeSelector,
} from "@/state/selectors"; } from "@/state";
import useGridSpans from "@/hooks/useGridSpans"; import useGridSpans from "@/hooks/useGridSpans";
import Notice from "@/components/Notice"; import Notice from "@/components/Notice";
@@ -26,6 +28,13 @@ const defaultSearchTags = [
"weather", "weather",
]; ];
const gridStyle: Record<string, CSSProperties> = {
light: {},
dark: {
backgroundColor: "#35313D",
},
} as const;
type IconGridProps = {}; type IconGridProps = {};
const IconGrid = (_: IconGridProps) => { const IconGrid = (_: IconGridProps) => {
@@ -56,15 +65,10 @@ const IconGrid = (_: IconGridProps) => {
<IconContext.Provider value={{ weight, size, color, mirrored: false }}> <IconContext.Provider value={{ weight, size, color, mirrored: false }}>
<div <div
className="grid-container" className="grid-container"
style={{ backgroundColor: isDark ? "#35313D" : "" }} style={isDark ? gridStyle.dark : gridStyle.light}
> >
<i id="beacon" className="beacon" /> <i id="beacon" className="beacon" />
<motion.div <motion.div className="grid" initial="hidden" animate={controls}>
className="grid"
initial="hidden"
animate={controls}
variants={{}}
>
{filteredQueryResults.map((iconEntry, index) => ( {filteredQueryResults.map((iconEntry, index) => (
<IconGridItem <IconGridItem
key={index} key={index}

View File

@@ -1,11 +1,9 @@
import { useRef, useLayoutEffect, useEffect, MutableRefObject } from "react"; import { useRef, useLayoutEffect, useEffect, MutableRefObject } from "react";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { motion, AnimatePresence } from "framer-motion"; import { motion } from "framer-motion";
import { IconEntry } from "@/lib"; import { IconEntry } from "@/lib";
import { iconPreviewOpenAtom, selectionEntryAtom } from "@/state/atoms"; import { selectionEntryAtom } from "@/state";
import DetailsPanel from "./DetailsPanel";
interface IconGridItemProps { interface IconGridItemProps {
index: number; index: number;
@@ -70,7 +68,7 @@ const IconGridItem = (props: IconGridItemProps) => {
className="grid-item" className="grid-item"
key={name} key={name}
ref={ref} ref={ref}
tabIndex={0} tabIndex={1}
style={{ style={{
order: index, order: index,
backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined, backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined,
@@ -88,9 +86,6 @@ const IconGridItem = (props: IconGridItemProps) => {
{isUpdated && <span className="badge updated"></span>} {isUpdated && <span className="badge updated"></span>}
</p> </p>
</motion.div> </motion.div>
{/* <AnimatePresence initial={false}>
{isOpen && <DetailsPanel {...props} />}
</AnimatePresence> */}
</> </>
); );
}; };

View File

@@ -1,7 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useSetRecoilState } from "recoil"; import { useSetRecoilState } from "recoil";
import { searchQueryAtom } from "@/state/atoms"; import { searchQueryAtom } from "@/state";
import "./TagCloud.css"; import "./TagCloud.css";
interface TagCloudProps { interface TagCloudProps {

View File

@@ -3,8 +3,7 @@ import { motion } from "framer-motion";
import { useRecoilValue } from "recoil"; import { useRecoilValue } from "recoil";
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react"; import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
import { isDarkThemeSelector } from "@/state/selectors"; import { searchQueryAtom, isDarkThemeSelector } from "@/state";
import { searchQueryAtom } from "@/state/atoms";
interface NoticeProps { interface NoticeProps {
message?: string; message?: string;

View File

@@ -11,7 +11,7 @@ import { useHotkeys } from "react-hotkeys-hook";
import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react"; import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { searchQueryAtom } from "@/state/atoms"; import { searchQueryAtom } from "@/state";
import "./SearchInput.css"; import "./SearchInput.css";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i; const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;

View File

@@ -1,9 +1,13 @@
import { useRecoilValue, useResetRecoilState } from "recoil"; import { useRecoilValue, useResetRecoilState } from "recoil";
import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react"; import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react";
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms"; import { useTransientState } from "@/hooks";
import useTransientState from "@/hooks/useTransientState"; import {
import { resetSettingsSelector } from "@/state/selectors"; iconWeightAtom,
iconSizeAtom,
iconColorAtom,
resetSettingsSelector,
} from "@/state";
import "./SettingsActions.css"; import "./SettingsActions.css";

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { iconSizeAtom } from "@/state/atoms"; import { iconSizeAtom } from "@/state";
import "./SizeInput.css"; import "./SizeInput.css";
type SizeInputProps = {}; type SizeInputProps = {};

View File

@@ -4,7 +4,7 @@ import Select from "react-dropdown-select";
import { PencilLine } from "phosphor-react"; import { PencilLine } from "phosphor-react";
import { IconStyle } from "@phosphor-icons/core"; import { IconStyle } from "@phosphor-icons/core";
import { iconWeightAtom } from "@/state/atoms"; import { iconWeightAtom } from "@/state";
import "./StyleInput.css"; import "./StyleInput.css";

View File

@@ -1,15 +1,12 @@
.tabs { .tabs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-left: 2px solid rgba(163, 159, 171, 0.1);
border-right: 2px solid rgba(163, 159, 171, 0.1);
} }
.tabs-header { .tabs-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
border-bottom: 2px solid rgba(163, 159, 171, 0.1);
} }
button.tab { button.tab {
@@ -19,14 +16,25 @@ button.tab {
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
flex: 1; flex: 1;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
button.tab:focus-within {
/* background-color: var(--tabs-background); */
} }
.tab.active { .tab.active {
background-color: rgba(194, 186, 196, 0.25); background-color: var(--tabs-background);
border-bottom: none;
} }
.tab-content { .tab-content {
flex: 1; flex: 1;
padding: 8px 16px;
display: grid; display: grid;
place-items: center; place-items: center;
border-radius: 8px;
background-color: var(--tabs-background);
overflow-y: auto;
} }

View File

@@ -1,4 +1,7 @@
import { ReactNode, useState } from "react"; import { CSSProperties, ReactNode, useState } from "react";
import { useRecoilValue } from "recoil";
import { isDarkThemeSelector } from "@/state";
import "./Tabs.css"; import "./Tabs.css";
@@ -11,15 +14,37 @@ type TabsProps = {
tabs: Tab[]; tabs: Tab[];
}; };
type CSSCustomPropertyName = `--${string}`;
type CSSCustomProperties = {
[property: CSSCustomPropertyName]: string;
};
const colorStyles: Record<string, CSSProperties & CSSCustomProperties> = {
light: { "--tabs-background": "white" },
dark: { "--tabs-background": "rgba(194, 186, 196, 0.25)" },
} as const;
const contentStyles: Record<string, CSSProperties> = {
activeLeft: { borderTopLeftRadius: 0 },
activeRight: { borderTopRightRadius: 0 },
} as const;
const Tabs = ({ tabs }: TabsProps) => { const Tabs = ({ tabs }: TabsProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0); const [activeIndex, setActiveIndex] = useState<number>(0);
const isDark = useRecoilValue(isDarkThemeSelector);
return ( return (
<div className="tabs"> <div
className="tabs"
tabIndex={0}
style={isDark ? colorStyles.dark : colorStyles.light}
>
<div className="tabs-header"> <div className="tabs-header">
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<button <button
key={i} key={i}
tabIndex={0}
className={`tab ${activeIndex === i ? "active" : ""}`} className={`tab ${activeIndex === i ? "active" : ""}`}
onClick={() => setActiveIndex(i)} onClick={() => setActiveIndex(i)}
> >
@@ -27,7 +52,18 @@ const Tabs = ({ tabs }: TabsProps) => {
</button> </button>
))} ))}
</div> </div>
<div className="tab-content">{tabs[activeIndex]?.content}</div> <div
className="tab-content"
style={
activeIndex === 0
? contentStyles.activeLeft
: activeIndex === tabs.length - 1
? contentStyles.activeRight
: undefined
}
>
{tabs[activeIndex]?.content}
</div>
</div> </div>
); );
}; };

9
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export { default as useDebounce } from "./useDebounce";
export { default as useEvent } from "./useEvent";
export { default as useIconParameters } from "./useIconParameters";
export { default as usePersistSettings } from "./usePersistSettings";
export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled";
export { default as useTimeoutFn } from "./useTimeoutFn";
export { default as useTransientState } from "./useTransientState";
export { default as useUnmount } from "./useUnmount";

45
src/hooks/useEvent.ts Normal file
View File

@@ -0,0 +1,45 @@
import { useEffect } from "react";
export type UseEventTarget = HTMLElement | SVGElement | Document | Window;
export type UseEventMap<E extends UseEventTarget> = E extends HTMLElement
? HTMLElementEventMap
: E extends SVGElement
? SVGElementEventMap
: E extends Document
? DocumentEventMap
: WindowEventMap;
export type UseEventType<E extends UseEventTarget> = keyof UseEventMap<E>;
/**
* Attach event listeners to arbitrary targets, and perform necessary cleanup
* when unmounting. Provides type inference for the listener based on the
* provided event name (currently supports {@link Window}, {@link Document},
* and subclasses of {@link HTMLElement} and {@link SVGElement}).
*
* @param type an {@link https://developer.mozilla.org/en-US/docs/Web/Events#event_listing event type}
* @param listener a callback to be fired on the event
* @param options {@link AddEventListenerOptions}
* @param el the target element to attack the listener. Defaults to
* {@link Document} when omitted.
*/
export default function useEvent<
K extends UseEventType<T>,
M extends UseEventMap<T>,
T extends UseEventTarget = Document
>(
type: K,
listener: (this: T, ev: M[K]) => any,
options?: boolean | AddEventListenerOptions,
el?: T
) {
useEffect(() => {
const target = el ?? document;
// @ts-ignore
target.addEventListener(type, listener, options);
// @ts-ignore
return () => target.removeEventListener(type, listener);
}, [el, type]);
}

2
src/state/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./atoms";
export * from "./selectors";