feat(app): new details footer appearance
This commit is contained in:
@@ -31,9 +31,9 @@ pre {
|
||||
box-sizing: border-box;
|
||||
padding: 20px 16px 20px 24px;
|
||||
margin: 12px 0px;
|
||||
background-color: white;
|
||||
/* background-color: white; */
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e1d4d7;
|
||||
/* border: 1px solid #e1d4d7; */
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -95,17 +95,6 @@ button.main-button svg {
|
||||
/* gap: 24px; */
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a.main-link {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
|
||||
import { iconColorAtom } from "@/state/atoms";
|
||||
import { isDarkThemeSelector } from "@/state/selectors";
|
||||
import useThrottled from "@/hooks/useThrottled";
|
||||
import { useThrottled } from "@/hooks";
|
||||
import { iconColorAtom, isDarkThemeSelector } from "@/state";
|
||||
|
||||
import "./ColorInput.css";
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ footer .links {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#back-to-top-button img {
|
||||
#back-to-top-button svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
@@ -4,27 +4,27 @@ import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||
import { Svg2Png } from "svg2png-converter";
|
||||
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 Tabs, { Tab } from "@/components/Tabs";
|
||||
import { useTransientState } from "@/hooks";
|
||||
import { SnippetType } from "@/lib";
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
selectionEntryAtom,
|
||||
} from "@/state/atoms";
|
||||
import { isDarkThemeSelector } from "@/state/selectors";
|
||||
import Tabs, { Tab } from "@/components/Tabs";
|
||||
import useTransientState from "@/hooks/useTransientState";
|
||||
import { SnippetType } from "@/lib";
|
||||
isDarkThemeSelector,
|
||||
} from "@/state";
|
||||
import { getCodeSnippets, supportsWeight } from "@/utils";
|
||||
|
||||
import TagCloud from "./TagCloud";
|
||||
|
||||
const variants: Variants = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
initial: { y: 188 },
|
||||
animate: { y: 0 },
|
||||
exit: { y: 188 },
|
||||
};
|
||||
|
||||
const RENDERED_SNIPPETS = [
|
||||
@@ -94,10 +94,7 @@ const DetailFooter = () => {
|
||||
header: type,
|
||||
content: (
|
||||
<div className="snippet" key={type}>
|
||||
<pre
|
||||
tabIndex={0}
|
||||
style={isWeightSupported ? undefined : snippetButtonStyle}
|
||||
>
|
||||
<pre style={isWeightSupported ? undefined : snippetButtonStyle}>
|
||||
<span>
|
||||
{isWeightSupported
|
||||
? snippets[type]
|
||||
@@ -213,31 +210,44 @@ const DetailFooter = () => {
|
||||
<div className="detail-preview">
|
||||
<figure>
|
||||
<entry.Icon ref={ref} size={64}></entry.Icon>
|
||||
<figcaption>{entry.name}</figcaption>
|
||||
</figure>
|
||||
<figcaption>
|
||||
<p>{entry.name}</p>
|
||||
<small className="versioning">
|
||||
in ≥ {entry.published_in.toFixed(1)}.0
|
||||
available in v{entry.published_in.toFixed(1)}.0+
|
||||
</small>
|
||||
</div>
|
||||
<Tabs tabs={tabs} />
|
||||
</figcaption>
|
||||
</figure>
|
||||
<div className="detail-actions">
|
||||
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
|
||||
<Download size={24} color="currentColor" weight="fill" /> Download
|
||||
PNG
|
||||
<button
|
||||
tabIndex={0}
|
||||
style={buttonBarStyle}
|
||||
onClick={handleDownloadPNG}
|
||||
>
|
||||
<DownloadSimple size={24} color="currentColor" weight="fill" /> PNG
|
||||
</button>
|
||||
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
|
||||
<Download size={24} color="currentColor" weight="fill" /> Download
|
||||
SVG
|
||||
<button
|
||||
tabIndex={0}
|
||||
style={buttonBarStyle}
|
||||
onClick={handleDownloadSVG}
|
||||
>
|
||||
<DownloadSimple size={24} color="currentColor" weight="fill" /> SVG
|
||||
</button>
|
||||
<button style={buttonBarStyle} onClick={handleCopySVG}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
style={buttonBarStyle}
|
||||
onClick={handleCopySVG}
|
||||
>
|
||||
{copied === "SVG" ? (
|
||||
<CheckCircle size={24} color={successColor} weight="fill" />
|
||||
) : (
|
||||
<Copy size={24} color="currentColor" weight="fill" />
|
||||
)}
|
||||
{copied === "SVG" ? "Copied!" : "Copy SVG"}
|
||||
{copied === "SVG" ? "Copied!" : " SVG"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs tabs={tabs} />
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -7,14 +7,14 @@ import { saveAs } from "file-saver";
|
||||
import { Copy, X, CheckCircle, Download } from "phosphor-react";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import { useTransientState } from "@/hooks";
|
||||
import { IconEntry, SnippetType } from "@/lib";
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
iconPreviewOpenAtom,
|
||||
} from "@/state/atoms";
|
||||
import useTransientState from "@/hooks/useTransientState";
|
||||
import { IconEntry, SnippetType } from "@/lib";
|
||||
} from "@/state";
|
||||
import { getCodeSnippets, supportsWeight } from "@/utils";
|
||||
|
||||
import TagCloud from "./TagCloud";
|
||||
|
||||
@@ -119,14 +119,15 @@
|
||||
}
|
||||
|
||||
.snippet {
|
||||
margin-bottom: 24px;
|
||||
/* margin-bottom: 24px; */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.snippet pre {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
color: black;
|
||||
/* color: black; */
|
||||
-moz-user-select: all;
|
||||
-webkit-user-select: all;
|
||||
user-select: all;
|
||||
@@ -218,16 +219,40 @@ aside.detail-footer {
|
||||
margin: auto;
|
||||
max-width: 1120px;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, CSSProperties } from "react";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { motion, useAnimation } from "framer-motion";
|
||||
import { IconContext } from "phosphor-react";
|
||||
|
||||
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms";
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
filteredQueryResultsSelector,
|
||||
isDarkThemeSelector,
|
||||
} from "@/state/selectors";
|
||||
} from "@/state";
|
||||
import useGridSpans from "@/hooks/useGridSpans";
|
||||
import Notice from "@/components/Notice";
|
||||
|
||||
@@ -26,6 +28,13 @@ const defaultSearchTags = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
const gridStyle: Record<string, CSSProperties> = {
|
||||
light: {},
|
||||
dark: {
|
||||
backgroundColor: "#35313D",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type IconGridProps = {};
|
||||
|
||||
const IconGrid = (_: IconGridProps) => {
|
||||
@@ -56,15 +65,10 @@ const IconGrid = (_: IconGridProps) => {
|
||||
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>
|
||||
<div
|
||||
className="grid-container"
|
||||
style={{ backgroundColor: isDark ? "#35313D" : "" }}
|
||||
style={isDark ? gridStyle.dark : gridStyle.light}
|
||||
>
|
||||
<i id="beacon" className="beacon" />
|
||||
<motion.div
|
||||
className="grid"
|
||||
initial="hidden"
|
||||
animate={controls}
|
||||
variants={{}}
|
||||
>
|
||||
<motion.div className="grid" initial="hidden" animate={controls}>
|
||||
{filteredQueryResults.map((iconEntry, index) => (
|
||||
<IconGridItem
|
||||
key={index}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useRef, useLayoutEffect, useEffect, MutableRefObject } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { IconEntry } from "@/lib";
|
||||
import { iconPreviewOpenAtom, selectionEntryAtom } from "@/state/atoms";
|
||||
|
||||
import DetailsPanel from "./DetailsPanel";
|
||||
import { selectionEntryAtom } from "@/state";
|
||||
|
||||
interface IconGridItemProps {
|
||||
index: number;
|
||||
@@ -70,7 +68,7 @@ const IconGridItem = (props: IconGridItemProps) => {
|
||||
className="grid-item"
|
||||
key={name}
|
||||
ref={ref}
|
||||
tabIndex={0}
|
||||
tabIndex={1}
|
||||
style={{
|
||||
order: index,
|
||||
backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined,
|
||||
@@ -88,9 +86,6 @@ const IconGridItem = (props: IconGridItemProps) => {
|
||||
{isUpdated && <span className="badge updated">•</span>}
|
||||
</p>
|
||||
</motion.div>
|
||||
{/* <AnimatePresence initial={false}>
|
||||
{isOpen && <DetailsPanel {...props} />}
|
||||
</AnimatePresence> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
|
||||
import { searchQueryAtom } from "@/state/atoms";
|
||||
import { searchQueryAtom } from "@/state";
|
||||
import "./TagCloud.css";
|
||||
|
||||
interface TagCloudProps {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { motion } from "framer-motion";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
|
||||
|
||||
import { isDarkThemeSelector } from "@/state/selectors";
|
||||
import { searchQueryAtom } from "@/state/atoms";
|
||||
import { searchQueryAtom, isDarkThemeSelector } from "@/state";
|
||||
|
||||
interface NoticeProps {
|
||||
message?: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import { searchQueryAtom } from "@/state/atoms";
|
||||
import { searchQueryAtom } from "@/state";
|
||||
import "./SearchInput.css";
|
||||
|
||||
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useRecoilValue, useResetRecoilState } from "recoil";
|
||||
import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react";
|
||||
|
||||
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms";
|
||||
import useTransientState from "@/hooks/useTransientState";
|
||||
import { resetSettingsSelector } from "@/state/selectors";
|
||||
import { useTransientState } from "@/hooks";
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
resetSettingsSelector,
|
||||
} from "@/state";
|
||||
|
||||
import "./SettingsActions.css";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
|
||||
import { iconSizeAtom } from "@/state/atoms";
|
||||
import { iconSizeAtom } from "@/state";
|
||||
import "./SizeInput.css";
|
||||
|
||||
type SizeInputProps = {};
|
||||
|
||||
@@ -4,7 +4,7 @@ import Select from "react-dropdown-select";
|
||||
import { PencilLine } from "phosphor-react";
|
||||
import { IconStyle } from "@phosphor-icons/core";
|
||||
|
||||
import { iconWeightAtom } from "@/state/atoms";
|
||||
import { iconWeightAtom } from "@/state";
|
||||
|
||||
import "./StyleInput.css";
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
.tabs {
|
||||
display: flex;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-bottom: 2px solid rgba(163, 159, 171, 0.1);
|
||||
}
|
||||
|
||||
button.tab {
|
||||
@@ -19,14 +16,25 @@ button.tab {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
button.tab:focus-within {
|
||||
/* background-color: var(--tabs-background); */
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background-color: rgba(194, 186, 196, 0.25);
|
||||
background-color: var(--tabs-background);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background-color: var(--tabs-background);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -11,15 +14,37 @@ type TabsProps = {
|
||||
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 [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
<div
|
||||
className="tabs"
|
||||
tabIndex={0}
|
||||
style={isDark ? colorStyles.dark : colorStyles.light}
|
||||
>
|
||||
<div className="tabs-header">
|
||||
{tabs.map((tab, i) => (
|
||||
<button
|
||||
key={i}
|
||||
tabIndex={0}
|
||||
className={`tab ${activeIndex === i ? "active" : ""}`}
|
||||
onClick={() => setActiveIndex(i)}
|
||||
>
|
||||
@@ -27,7 +52,18 @@ const Tabs = ({ tabs }: TabsProps) => {
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
9
src/hooks/index.ts
Normal file
9
src/hooks/index.ts
Normal 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
45
src/hooks/useEvent.ts
Normal 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
2
src/state/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./atoms";
|
||||
export * from "./selectors";
|
||||
Reference in New Issue
Block a user