feat(app): new details footer appearance
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ≥ {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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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> */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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