feat(app): recipe ideas
This commit is contained in:
@@ -8,6 +8,7 @@ import IconGrid from "@/components/IconGrid";
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import Notice from "@/components/Notice";
|
import Notice from "@/components/Notice";
|
||||||
|
import Recipes from "@/components/Recipes";
|
||||||
import {
|
import {
|
||||||
useIconParameters,
|
useIconParameters,
|
||||||
usePersistSettings,
|
usePersistSettings,
|
||||||
@@ -21,7 +22,7 @@ const waitingFallback = <Notice type="none" message="" />;
|
|||||||
const App: React.FC<any> = () => {
|
const App: React.FC<any> = () => {
|
||||||
useIconParameters();
|
useIconParameters();
|
||||||
usePersistSettings();
|
usePersistSettings();
|
||||||
|
|
||||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||||
|
|
||||||
const properties = useMemo(
|
const properties = useMemo(
|
||||||
@@ -47,6 +48,7 @@ const App: React.FC<any> = () => {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
|
<Recipes />
|
||||||
<Footer />
|
<Footer />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
22
src/components/Recipes/Recipe.tsx
Normal file
22
src/components/Recipes/Recipe.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ArrowCircleUpRight } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
export type RecipeProps = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
Example: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Recipe = ({ title, url, Example }: RecipeProps) => {
|
||||||
|
return (
|
||||||
|
<a className="recipe card" href={url}>
|
||||||
|
{/* <h1>{title}</h1> */}
|
||||||
|
<div className="recipe-linkout">
|
||||||
|
<span>Open on StackBlitz</span>
|
||||||
|
<ArrowCircleUpRight weight="fill" size={32} />
|
||||||
|
</div>
|
||||||
|
<Example />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Recipe;
|
||||||
39
src/components/Recipes/Recipes.css
Normal file
39
src/components/Recipes/Recipes.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.recipes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(184px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding-block: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.recipe {
|
||||||
|
position: relative;
|
||||||
|
color: initial;
|
||||||
|
padding: 16px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-linkout {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background-color: var(--soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-linkout:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 64px;
|
||||||
|
gap: 12px;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
29
src/components/Recipes/Recipes.tsx
Normal file
29
src/components/Recipes/Recipes.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { IconContext } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
import Recipe from "./Recipe";
|
||||||
|
import items from "./items";
|
||||||
|
import "./Recipes.css";
|
||||||
|
|
||||||
|
const Recipes = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="toolbar">
|
||||||
|
<div className="toolbar-contents">
|
||||||
|
<h2>Recipes</h2>
|
||||||
|
<p>Cool stuff to do with Phosphor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-container">
|
||||||
|
<IconContext.Provider value={{ size: 64 }}>
|
||||||
|
<div className="recipes grid">
|
||||||
|
{items.map((itemProps) => (
|
||||||
|
<Recipe key={itemProps.title} {...itemProps} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</IconContext.Provider>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Recipes;
|
||||||
1
src/components/Recipes/index.ts
Normal file
1
src/components/Recipes/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Recipes";
|
||||||
83
src/components/Recipes/items/Animation.tsx
Normal file
83
src/components/Recipes/items/Animation.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Cube } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
import { RecipeProps } from "../Recipe";
|
||||||
|
|
||||||
|
const recipe: RecipeProps = {
|
||||||
|
title: "SVG Wizardry",
|
||||||
|
url: "https://stackblitz.com/edit/react-ts-f7q7gs?file=App.tsx,style.css",
|
||||||
|
Example() {
|
||||||
|
return (
|
||||||
|
<div className="example">
|
||||||
|
<Cube
|
||||||
|
color="darkorchid"
|
||||||
|
weight="duotone"
|
||||||
|
style={{ fill: "url(#star)" }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id="star" viewBox="0,0,10,10" width="10%" height="10%">
|
||||||
|
<polygon
|
||||||
|
points="0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2"
|
||||||
|
fill="darkOrchid"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
</Cube>
|
||||||
|
<Cube
|
||||||
|
color="darkorchid"
|
||||||
|
weight="duotone"
|
||||||
|
style={{ filter: "url(#emboss)" }}
|
||||||
|
>
|
||||||
|
<filter id="emboss">
|
||||||
|
<feConvolveMatrix
|
||||||
|
kernelMatrix="
|
||||||
|
5 0 0
|
||||||
|
0 0 0
|
||||||
|
0 0 -3
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</filter>
|
||||||
|
</Cube>
|
||||||
|
<Cube color="darkorchid" weight="duotone">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0;1;0"
|
||||||
|
dur="4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="5s"
|
||||||
|
from="0 0 0"
|
||||||
|
to="360 0 0"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
></animateTransform>
|
||||||
|
</Cube>
|
||||||
|
<Cube
|
||||||
|
color="darkorchid"
|
||||||
|
weight="duotone"
|
||||||
|
style={{ filter: "url(#displacementFilter)" }}
|
||||||
|
>
|
||||||
|
<filter id="displacementFilter">
|
||||||
|
<feTurbulence
|
||||||
|
type="turbulence"
|
||||||
|
baseFrequency="0.01"
|
||||||
|
numOctaves="3"
|
||||||
|
result="turbulence"
|
||||||
|
/>
|
||||||
|
<feDisplacementMap
|
||||||
|
in2="turbulence"
|
||||||
|
in="SourceGraphic"
|
||||||
|
scale="20"
|
||||||
|
xChannelSelector="B"
|
||||||
|
yChannelSelector="G"
|
||||||
|
/>
|
||||||
|
</filter>
|
||||||
|
</Cube>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default recipe;
|
||||||
63
src/components/Recipes/items/Duocolor.tsx
Normal file
63
src/components/Recipes/items/Duocolor.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
IconProps,
|
||||||
|
Barricade,
|
||||||
|
GasCan,
|
||||||
|
IceCream,
|
||||||
|
FlyingSaucer,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
import { RecipeProps } from "../Recipe";
|
||||||
|
|
||||||
|
type DuocolorProps = Omit<IconProps, "weight"> & {
|
||||||
|
Icon: Icon;
|
||||||
|
duocolor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Duocolor({ Icon, duocolor, ...iconProps }: DuocolorProps) {
|
||||||
|
const [uuid, style] = useMemo(() => {
|
||||||
|
// UUID to make sure the inline stylesheet is "scoped" to this icon only.
|
||||||
|
// Could also easily be implemented with a regular CSS selector.
|
||||||
|
const uuid = "ph-" + Math.floor(Math.random() * 1_000_000).toString(16);
|
||||||
|
// const uuid = "ph-" + crypto.randomUUID();
|
||||||
|
return [uuid, !duocolor ? null : createDuocolorStyle(uuid, duocolor)];
|
||||||
|
}, [duocolor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{style}
|
||||||
|
<Icon {...iconProps} weight="duotone" data-ph={uuid} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDuocolorStyle(id: string, color: string) {
|
||||||
|
return (
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
[data-ph="${id}"] [opacity="0.2"] {
|
||||||
|
opacity: 1;
|
||||||
|
fill: ${color};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipe: RecipeProps = {
|
||||||
|
title: "Duocolor",
|
||||||
|
url: "https://stackblitz.com/edit/react-ts-kvdzu1?file=App.tsx",
|
||||||
|
Example() {
|
||||||
|
return (
|
||||||
|
<div className="example">
|
||||||
|
<Duocolor Icon={FlyingSaucer} duocolor="darkcyan" />
|
||||||
|
<Duocolor Icon={Barricade} color="darkgray" duocolor="orange" />
|
||||||
|
<Duocolor Icon={IceCream} color="saddlebrown" duocolor="lightpink" />
|
||||||
|
<Duocolor Icon={GasCan} duocolor="indianred" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default recipe;
|
||||||
6
src/components/Recipes/items/index.ts
Normal file
6
src/components/Recipes/items/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import animation from "./Animation";
|
||||||
|
import duocolor from "./Duocolor";
|
||||||
|
import { RecipeProps } from "../Recipe";
|
||||||
|
|
||||||
|
const items: RecipeProps[] = [animation, duocolor, duocolor, animation];
|
||||||
|
export default items;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
nav.toolbar {
|
.toolbar {
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: white;
|
||||||
background-color: var(--eggplant);
|
background-color: var(--eggplant);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user