├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── api
│ └── comments
│ │ └── route.ts
├── experiments
│ └── family-browser-icon
│ │ ├── icon.tsx
│ │ └── page.tsx
├── fonts
│ ├── intelone-mono
│ │ └── regular.woff
│ └── pp-editorial-new
│ │ ├── bold.woff
│ │ ├── bold.woff2
│ │ ├── regular.woff
│ │ └── regular.woff2
├── layout.tsx
├── styles.css
└── svg-paths
│ ├── arcs
│ ├── content.tsx
│ ├── content
│ │ ├── arc-sandbox.md
│ │ ├── arc-sandbox.tsx
│ │ ├── ellipse.tsx
│ │ ├── flags.tsx
│ │ ├── rotation.tsx
│ │ ├── small-ellipse.tsx
│ │ ├── syntax.tsx
│ │ └── types.ts
│ ├── index.mdx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── state.ts
│ ├── bezier-curves
│ ├── content.tsx
│ ├── index.mdx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── state.ts
│ ├── challenge
│ ├── content.tsx
│ ├── index.mdx
│ ├── opengraph-image.png
│ └── page.tsx
│ ├── components
│ ├── README.md
│ ├── button.tsx
│ ├── command-list.tsx
│ ├── draggable-endpoint.tsx
│ ├── hooks
│ │ └── use-media-query.ts
│ ├── icons.tsx
│ ├── index-provider.tsx
│ ├── interactive-command.tsx
│ ├── mdx.tsx
│ ├── page-section.module.css
│ ├── page-section.tsx
│ ├── path-editor.tsx
│ ├── path-hover-visual.tsx
│ ├── path-practice.tsx
│ ├── path-visualizer.tsx
│ ├── path.tsx
│ ├── play-slider.tsx
│ ├── popover.tsx
│ ├── ripple.tsx
│ ├── slider.tsx
│ ├── state-context.tsx
│ ├── svg.tsx
│ ├── svg
│ │ ├── circle.tsx
│ │ ├── drag-group.tsx
│ │ ├── line.tsx
│ │ ├── path.tsx
│ │ ├── text.tsx
│ │ └── tooltip.tsx
│ ├── utils.tsx
│ └── visual-wrapper.tsx
│ ├── cubic-curves
│ ├── content.tsx
│ ├── content
│ │ ├── chain.tsx
│ │ ├── cubic-playground.tsx
│ │ ├── curve-general.tsx
│ │ ├── pill.tsx
│ │ └── syntax.tsx
│ ├── index.mdx
│ ├── opengraph-image.png
│ ├── page.tsx
│ ├── state.ts
│ └── types.ts
│ ├── cursors
│ ├── content.tsx
│ ├── cursors.tsx
│ ├── index.mdx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── state.ts
│ ├── index
│ ├── absolute.tsx
│ ├── commands.tsx
│ ├── content.tsx
│ ├── heart.tsx
│ ├── index.mdx
│ ├── square.tsx
│ └── state.ts
│ ├── layout.tsx
│ ├── lib
│ ├── fs.ts
│ ├── path.test.ts
│ └── path.ts
│ ├── lines
│ ├── components.tsx
│ ├── content.tsx
│ ├── index.mdx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── state.ts
│ ├── opengraph-image.png
│ ├── page.tsx
│ ├── provider.tsx
│ └── utils.ts
├── assets
├── dark-default.json
└── light-colorblind.json
├── bin
├── component.js
└── stories.js
├── components
├── AlgorithmControls.tsx
├── Callout.tsx
├── Caption.tsx
├── ChangeIndicator.tsx
├── Checkbox
│ ├── Checkbox.stories.tsx
│ ├── Checkbox.tsx
│ └── index.tsx
├── Content.tsx
├── Figure.tsx
├── FullWidth.tsx
├── Grid.tsx
├── Heading.tsx
├── Image.tsx
├── Img.tsx
├── Link
│ ├── Link.stories.tsx
│ ├── Link.tsx
│ └── index.tsx
├── MobileBottomBar.tsx
├── MobileNavIsland
│ ├── MobileNavIsland.stories.tsx
│ ├── MobileNavIsland.tsx
│ └── index.tsx
├── NewsletterForm.tsx
├── Note.tsx
├── OrderedList.tsx
├── PlayButton.tsx
├── Post.tsx
├── PrimaryButton
│ ├── PrimaryButton.stories.tsx
│ ├── PrimaryButton.tsx
│ └── index.tsx
├── ProblemStatement.tsx
├── Quiz
│ ├── Quiz.stories.tsx
│ ├── Quiz.tsx
│ └── index.tsx
├── Sandbox
│ ├── Sandbox.stories.tsx
│ ├── Sandbox.tsx
│ ├── codemirror.ts
│ └── index.tsx
├── SecondaryButton.tsx
├── SharedState.tsx
├── Slider.tsx
├── Spoiler.tsx
├── SubscribeButton
│ ├── SubscribeButton.stories.tsx
│ ├── SubscribeButton.tsx
│ └── index.tsx
├── SubscribeInput.tsx
├── SvgGridWrapper
│ ├── SvgGridWrapper.stories.tsx
│ ├── SvgGridWrapper.tsx
│ └── index.tsx
├── ThemeToggle.tsx
├── Visualizer
│ ├── Visualizer.stories.tsx
│ ├── Visualizer.tsx
│ └── index.tsx
├── home
│ ├── Debugger
│ │ ├── Debugger.stories.tsx
│ │ ├── Debugger.tsx
│ │ └── index.tsx
│ ├── FramerMagicMotion
│ │ ├── FramerMagicMotion.stories.tsx
│ │ ├── FramerMagicMotion.tsx
│ │ └── index.tsx
│ ├── FramerMotionKeys
│ │ ├── FramerMotionKeys.stories.tsx
│ │ ├── FramerMotionKeys.tsx
│ │ └── index.tsx
│ ├── HowArraysWork
│ │ ├── HowArraysWork.stories.tsx
│ │ ├── HowArraysWork.tsx
│ │ └── index.tsx
│ ├── SlidingWindow
│ │ ├── SlidingWindow.stories.tsx
│ │ ├── SlidingWindow.tsx
│ │ └── index.tsx
│ ├── SvgPaths
│ │ ├── SvgPaths.stories.tsx
│ │ ├── SvgPaths.tsx
│ │ └── index.tsx
│ ├── TokenizerVisual
│ │ ├── TokenizerVisual.stories.tsx
│ │ ├── TokenizerVisual.tsx
│ │ └── index.tsx
│ └── shared.tsx
├── layout
│ ├── ExperimentsPage.tsx
│ ├── Row.tsx
│ └── StoriesPage.tsx
└── utils
│ └── SvgBackgroundGradient.tsx
├── content
├── aoc
│ └── 2022
│ │ └── day-01
│ │ └── index.tsx
├── keys-in-framer-motion
│ ├── components
│ │ ├── AnimationShowcase
│ │ │ ├── AnimationShowcase.stories.tsx
│ │ │ ├── AnimationShowcase.tsx
│ │ │ └── index.tsx
│ │ ├── CodeQuiz
│ │ │ ├── CodeQuiz.tsx
│ │ │ └── index.tsx
│ │ ├── Counter
│ │ │ ├── Counter.tsx
│ │ │ └── index.tsx
│ │ ├── KanjiCarousel
│ │ │ ├── KanjiCarousel.stories.tsx
│ │ │ ├── KanjiCarousel.tsx
│ │ │ ├── KanjiCarouselSlide.tsx
│ │ │ └── index.tsx
│ │ ├── NextButton
│ │ │ ├── NextButton.stories.tsx
│ │ │ ├── NextButton.tsx
│ │ │ ├── Sandbox.tsx
│ │ │ └── index.tsx
│ │ └── RefreshComponent.tsx
│ └── index.mdx
├── magic-motion
│ ├── components
│ │ ├── CorrectedInverseAnimation
│ │ │ ├── CorrectedInverseAnimation.stories.tsx
│ │ │ ├── CorrectedInverseAnimation.tsx
│ │ │ └── index.tsx
│ │ ├── FlipExample
│ │ │ ├── FlipExample.stories.tsx
│ │ │ ├── FlipExample.tsx
│ │ │ └── index.tsx
│ │ ├── FlipFirst
│ │ │ ├── FlipFirst.stories.tsx
│ │ │ ├── FlipFirst.tsx
│ │ │ ├── index.ts
│ │ │ └── machine.ts
│ │ ├── FlipInverse
│ │ │ ├── FlipInverse.stories.tsx
│ │ │ ├── FlipInverse.tsx
│ │ │ └── index.tsx
│ │ ├── FlipLast
│ │ │ ├── FlipLast.stories.tsx
│ │ │ ├── FlipLast.tsx
│ │ │ ├── index.ts
│ │ │ └── machine.ts
│ │ ├── FlipLastReact
│ │ │ ├── FlipLastReact.stories.tsx
│ │ │ ├── FlipLastReact.tsx
│ │ │ └── index.tsx
│ │ ├── FlipOverview
│ │ │ ├── FlipOverview.stories.tsx
│ │ │ ├── FlipOverview.tsx
│ │ │ ├── index.tsx
│ │ │ └── machine.ts
│ │ ├── FlipPlay
│ │ │ ├── FlipPlay.stories.tsx
│ │ │ ├── FlipPlay.tsx
│ │ │ └── index.tsx
│ │ ├── InitialPositionSandbox
│ │ │ ├── InitialPositionSandbox.stories.tsx
│ │ │ ├── InitialPositionSandbox.tsx
│ │ │ └── index.tsx
│ │ ├── InverseSandbox
│ │ │ ├── InverseSandbox.stories.tsx
│ │ │ ├── InverseSandbox.tsx
│ │ │ └── index.tsx
│ │ ├── InverseScaleFormula
│ │ │ ├── InverseScaleFormula.stories.tsx
│ │ │ ├── InverseScaleFormula.tsx
│ │ │ └── index.tsx
│ │ ├── InverseScaleGraph
│ │ │ ├── InverseScaleGraph.stories.tsx
│ │ │ ├── InverseScaleGraph.tsx
│ │ │ └── index.tsx
│ │ ├── InverseScaleSandbox
│ │ │ ├── InverseScaleSandbox.stories.tsx
│ │ │ ├── InverseScaleSandbox.tsx
│ │ │ └── index.tsx
│ │ ├── InverseSizeSlider
│ │ │ ├── InverseSizeSlider.stories.tsx
│ │ │ ├── InverseSizeSlider.tsx
│ │ │ └── index.tsx
│ │ ├── LayoutChangeExample
│ │ │ ├── LayoutChangeExample.stories.tsx
│ │ │ ├── LayoutChangeExample.tsx
│ │ │ └── index.tsx
│ │ ├── Motion
│ │ │ ├── Motion.stories.tsx
│ │ │ ├── Motion.tsx
│ │ │ └── index.tsx
│ │ ├── MotionSandbox
│ │ │ ├── MotionSandbox.stories.tsx
│ │ │ ├── MotionSandbox.tsx
│ │ │ └── index.tsx
│ │ ├── MotionSquare
│ │ │ ├── MotionSquare.stories.tsx
│ │ │ ├── MotionSquare.tsx
│ │ │ └── index.tsx
│ │ ├── PlaySandbox
│ │ │ ├── PlaySandbox.stories.tsx
│ │ │ ├── PlaySandbox.tsx
│ │ │ └── index.tsx
│ │ ├── SizeDistanceExample
│ │ │ ├── SizeDistanceExample.stories.tsx
│ │ │ ├── SizeDistanceExample.tsx
│ │ │ └── index.tsx
│ │ ├── SizeDistanceInverseSnapshot
│ │ │ ├── SizeDistanceInverseSnapshot.stories.tsx
│ │ │ ├── SizeDistanceInverseSnapshot.tsx
│ │ │ └── index.tsx
│ │ ├── SizeDistanceRelationship
│ │ │ ├── SizeDistanceRelationship.stories.tsx
│ │ │ ├── SizeDistanceRelationship.tsx
│ │ │ └── index.tsx
│ │ ├── SizeLayoutExample
│ │ │ ├── SizeLayoutExample.stories.tsx
│ │ │ ├── SizeLayoutExample.tsx
│ │ │ └── index.tsx
│ │ ├── SizeMeasurements
│ │ │ ├── SizeMeasurements.stories.tsx
│ │ │ ├── SizeMeasurements.tsx
│ │ │ └── index.tsx
│ │ ├── SizePlayAnimation
│ │ │ ├── SizePlayAnimation.stories.tsx
│ │ │ ├── SizePlayAnimation.tsx
│ │ │ └── index.tsx
│ │ ├── WidthTransitionSandbox
│ │ │ ├── WidthTransitionSandbox.stories.tsx
│ │ │ ├── WidthTransitionSandbox.tsx
│ │ │ └── index.tsx
│ │ ├── shared.tsx
│ │ ├── shared
│ │ │ ├── HorizontalRuler
│ │ │ │ ├── HorizontalRuler.stories.tsx
│ │ │ │ ├── HorizontalRuler.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Ruler.tsx
│ │ │ ├── SizeDiagram
│ │ │ │ ├── SizeDiagram.stories.tsx
│ │ │ │ ├── SizeDiagram.tsx
│ │ │ │ └── index.tsx
│ │ │ └── styles.tsx
│ │ └── size.tsx
│ └── index.mdx
└── tokenizer
│ ├── components
│ ├── Boilerplate.tsx
│ ├── CharacterList.tsx
│ ├── Pipeline.tsx
│ ├── TokenList.tsx
│ ├── Tokenizer
│ │ ├── Tokenizer.stories.tsx
│ │ ├── Tokenizer.tsx
│ │ └── index.ts
│ └── lib
│ │ ├── single-character.ts
│ │ └── tokenize.ts
│ └── index.mdx
├── experiments
├── BezierCurveQuiz
│ ├── BezierCurveQuiz.stories.tsx
│ ├── BezierCurveQuiz.tsx
│ └── index.tsx
├── Cards
│ ├── Cards.stories.tsx
│ ├── Cards.tsx
│ └── index.tsx
├── ControllableAnimation
│ ├── ControllableAnimation.stories.tsx
│ ├── ControllableAnimation.tsx
│ └── index.tsx
├── EasingCurveEditor
│ ├── EasingCurveEditor.stories.tsx
│ ├── EasingCurveEditor.tsx
│ └── index.tsx
├── EasingFunctionSandbox
│ ├── EasingFunctionSandbox.stories.tsx
│ ├── EasingFunctionSandbox.tsx
│ └── index.tsx
├── TextLoadAnimation
│ ├── TextLoadAnimation.stories.tsx
│ ├── TextLoadAnimation.tsx
│ ├── alphanumeric.tsx
│ └── index.tsx
└── TopDownParser
│ ├── TopDownParser.stories.tsx
│ ├── TopDownParser.tsx
│ └── index.tsx
├── lib
├── algorithm
│ ├── exec.ts
│ ├── index.ts
│ ├── snapshot.macro.d.ts
│ ├── snapshot.macro.js
│ ├── types.ts
│ └── use-algorithm.ts
├── config.ts
├── content.server.ts
├── highlighter.ts
├── use-interval.ts
└── utils.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
├── [content].tsx
├── _app.tsx
├── _document.tsx
├── _stories
│ ├── [name].tsx
│ └── index.tsx
├── api
│ ├── auth
│ │ └── [...nextauth].ts
│ └── subscribe.ts
├── experiments
│ ├── bezier-curves.tsx
│ ├── cards.tsx
│ ├── family-input.tsx
│ ├── heavy-square.tsx
│ ├── path-animation.tsx
│ ├── safari-scroll.tsx
│ └── scrambled-text.tsx
├── index.tsx
└── stack-overflow.mdx
├── postcss.config.js
├── public
├── bg-dark.svg
├── favicon.ico
├── fonts
│ └── pp-editorial-new
│ │ ├── PPEditorialNew-Bold.woff
│ │ ├── PPEditorialNew-Bold.woff2
│ │ ├── PPEditorialNew-Regular.woff
│ │ └── PPEditorialNew-Regular.woff2
├── grid-dark.svg
├── grid.svg
├── noise.png
├── og
│ ├── debugger.png
│ ├── how-arrays-work.png
│ ├── index.png
│ ├── keys-in-framer-motion.png
│ ├── magic-motion.png
│ ├── sliding-window.png
│ └── tokenizer.png
├── shiki
│ ├── dark-default.json
│ ├── languages
│ │ ├── css.tmLanguage.json
│ │ ├── html.tmLanguage.json
│ │ ├── javascript.tmLanguage.json
│ │ ├── jsx.tmLanguage.json
│ │ ├── tsx.tmLanguage.json
│ │ └── typescript.tmLanguage.json
│ └── onig.wasm
├── tokenizer
│ ├── logo.svg
│ ├── pipeline.svg
│ ├── single-tokens.svg
│ ├── token-types.svg
│ └── tokens.svg
└── vercel.svg
├── stitches.config.ts
├── stories.meta.ts
├── styles
└── fonts.css
├── tailwind.config.js
├── tsconfig-esbuild.json
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["module-resolver", {
4 | "root": ["."],
5 | "alias": {
6 | "^~/(.+)": "./\\1"
7 | }
8 | }],
9 | "@babel/plugin-syntax-jsx",
10 | "macros"
11 | ],
12 | "presets": [
13 | "@babel/preset-typescript"
14 | ]
15 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "react/no-unescaped-entities": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # generated files
35 | _dist-content/
36 | old-src/
37 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "grammarly.selectors": [
4 | {
5 | "language": "mdx",
6 | "scheme": "file"
7 | }
8 | ],
9 | "typescript.enablePromptUseWorkspaceTsdk": true
10 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/app/experiments/family-browser-icon/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Icon } from "./icon";
3 |
4 | export const metadata = {
5 | title: "Not a Number | Experiments",
6 | };
7 |
8 | export default function FamilyBrowserIconPage() {
9 | return (
10 |
11 |
12 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/app/fonts/intelone-mono/regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/fonts/intelone-mono/regular.woff
--------------------------------------------------------------------------------
/app/fonts/pp-editorial-new/bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/fonts/pp-editorial-new/bold.woff
--------------------------------------------------------------------------------
/app/fonts/pp-editorial-new/bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/fonts/pp-editorial-new/bold.woff2
--------------------------------------------------------------------------------
/app/fonts/pp-editorial-new/regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/fonts/pp-editorial-new/regular.woff
--------------------------------------------------------------------------------
/app/fonts/pp-editorial-new/regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/fonts/pp-editorial-new/regular.woff2
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Nunito } from "next/font/google";
2 | import localFont from "next/font/local";
3 | import { clsx } from "clsx";
4 | import "./styles.css";
5 | import { AuthProvider } from "./svg-paths/provider";
6 | import { Analytics } from "@vercel/analytics/react";
7 |
8 | export const metadata = {
9 | title: "Next.js",
10 | description: "Generated by Next.js",
11 | };
12 |
13 | const sans = Nunito({
14 | weight: ["400", "600", "700"],
15 | variable: "--font-sans",
16 | subsets: ["latin"],
17 | });
18 |
19 | const mono = localFont({
20 | src: [
21 | {
22 | path: "./fonts/intelone-mono/regular.woff",
23 | weight: "400",
24 | style: "normal",
25 | },
26 | ],
27 | variable: "--font-mono",
28 | });
29 |
30 | const serif = localFont({
31 | src: [
32 | {
33 | path: "./fonts/pp-editorial-new/regular.woff2",
34 | weight: "400",
35 | style: "normal",
36 | },
37 | {
38 | path: "./fonts/pp-editorial-new/bold.woff2",
39 | weight: "700",
40 | style: "normal",
41 | },
42 | ],
43 | variable: "--font-serif",
44 | });
45 |
46 | export default function RootLayout({
47 | children,
48 | }: {
49 | children: React.ReactNode;
50 | }) {
51 | return (
52 |
53 |
61 | {children}
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/app/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .inline-code:not(pre > code) {
6 | @apply bg-gray7 px-1 text-sm;
7 | }
8 |
--------------------------------------------------------------------------------
/app/svg-paths/arcs/content/arc-sandbox.md:
--------------------------------------------------------------------------------
1 | The ideal API for this would be:
2 |
3 | ```tsx
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
--------------------------------------------------------------------------------
/app/svg-paths/arcs/content/rotation.tsx:
--------------------------------------------------------------------------------
1 | import { useStateContext } from "../state";
2 | import { parsePath } from "app/svg-paths/lib/path";
3 | import * as Arc from "./arc-sandbox";
4 | import { SyntaxState } from "./types";
5 |
6 | const path = parsePath("M 3 10 A 10 7.5 30 0 0 20 17");
7 |
8 | export const initialState = {
9 | slice: [1],
10 | active: ["1.xAxisRotation"],
11 | path,
12 | };
13 |
14 | export const components = { Rotation };
15 |
16 | function Rotation() {
17 | const { data, set } = useStateContext("rotation");
18 | return (
19 | <>
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 | );
35 | }
36 |
37 | export const page = {
38 | children: ,
39 | };
40 |
--------------------------------------------------------------------------------
/app/svg-paths/arcs/content/small-ellipse.tsx:
--------------------------------------------------------------------------------
1 | import { useStateContext } from "../state";
2 | import { useSvgContext } from "app/svg-paths/components/svg";
3 | import { Tooltip } from "app/svg-paths/components/svg/tooltip";
4 | import { parsePath } from "app/svg-paths/lib/path";
5 | import * as Arc from "./arc-sandbox";
6 | import { SyntaxState } from "./types";
7 |
8 | const path = parsePath("M 3 10 A 10 7.5 0 0 0 20 17");
9 |
10 | export const initialState = { path };
11 |
12 | export const components = { SmallEllipse };
13 |
14 | function SmallEllipse() {
15 | const { getRelative } = useSvgContext();
16 | const { data, set } = useStateContext("small-ellipse");
17 | const { path } = data;
18 | const arc = path.atAbsolute<"A">(1);
19 | return (
20 | <>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
36 | {arc.rx.toFixed(1)}
37 |
38 | >
39 | );
40 | }
41 |
42 | export const page = {
43 | children: ,
44 | };
45 |
--------------------------------------------------------------------------------
/app/svg-paths/arcs/content/types.ts:
--------------------------------------------------------------------------------
1 | import type { Path } from "app/svg-paths/lib/path";
2 | import type { DragGroupState } from "../../components/svg/drag-group";
3 |
4 | export type SyntaxState = {
5 | path: Path;
6 | } & DragGroupState;
7 |
--------------------------------------------------------------------------------
/app/svg-paths/arcs/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/arcs/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/arcs/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 | import { readPage } from "../lib/fs";
4 | import { Content } from "./content";
5 |
6 | export const metadata: Metadata = {
7 | title: "SVG Path Commands | Arcs",
8 | description: `The arc command is the least intuitive of the SVG path commands; let's take a look at how to use it to draw arcs.`,
9 | authors: [
10 | {
11 | name: "Nanda Syahrasyad",
12 | url: "https://twitter.com/nandafyi",
13 | },
14 | ],
15 | twitter: {
16 | card: "summary_large_image",
17 | title: "SVG Path Commands | Arcs",
18 | description: `The arc command is the least intuitive of the SVG path commands; let's take a look at how to use it to draw arcs.`,
19 | creator: "@nandafyi",
20 | },
21 | openGraph: {
22 | title: "SVG Path Commands | Arcs",
23 | description: `The arc command is the least intuitive of the SVG path commands; let's take a look at how to use it to draw arcs.`,
24 | url: "https://nan.fyi/svg-paths",
25 | siteName: "Not a Number",
26 | },
27 | };
28 |
29 | export default async function ArcsPage() {
30 | const { content, length } = await readPage("arcs");
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/arcs/state.ts:
--------------------------------------------------------------------------------
1 | import { useStateContext as useStateContextBase } from "../components/state-context";
2 | import { getInitialPracticeQuestionState } from "../components/path-practice";
3 | import { parsePath } from "../lib/path";
4 | import * as ellipse from "./content/ellipse";
5 | import * as smallEllipse from "./content/small-ellipse";
6 | import * as rotation from "./content/rotation";
7 | import * as flags from "./content/flags";
8 | import { createInitialState } from "../components/svg/drag-group";
9 |
10 | const practicePath = parsePath(
11 | "M 3 15 q 1.5 -2 1.5 -5 q 0 -2 1.5 -4 M 8 4 a 8 8 0 0 1 12 6 q 0.5 4 -2 9 M 13 21 q 1.5 -2 2 -5 M 16 12 v -1 a 4 4 0 0 0 -8 0 q 0 4 -2.5 7 M 8.5 20 q 3 -3 3.5 -9"
12 | );
13 |
14 | const path = parsePath("M 3 10 A 10 7.5 0 0 0 20 15");
15 |
16 | export const syntaxState = {
17 | ...createInitialState(),
18 | index: null,
19 | expanded: false,
20 | path,
21 | };
22 |
23 | const state = {
24 | syntax: syntaxState,
25 | ellipse: ellipse.initialState,
26 | "small-ellipse": smallEllipse.initialState,
27 | rotation: rotation.initialState,
28 | flags: flags.initialState,
29 | ...getInitialPracticeQuestionState(practicePath),
30 | };
31 |
32 | export const initialState = state;
33 |
34 | export const useStateContext = (
35 | key: Key
36 | ) => {
37 | return useStateContextBase()(key);
38 | };
39 |
--------------------------------------------------------------------------------
/app/svg-paths/bezier-curves/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/bezier-curves/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/bezier-curves/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 | import { readPage } from "../lib/fs";
4 | import { Content } from "./content";
5 |
6 | export const metadata: Metadata = {
7 | title: "SVG Path Commands | Bezier Curves",
8 | description: `The real power of SVG paths lies in its ability to draw curves. Let's take a look at the first type of curve: the quadratic bezier curve.`,
9 | authors: [
10 | {
11 | name: "Nanda Syahrasyad",
12 | url: "https://twitter.com/nandafyi",
13 | },
14 | ],
15 | twitter: {
16 | card: "summary_large_image",
17 | title: "SVG Path Commands | Bezier Curves",
18 | description: `The real power of SVG paths lies in its ability to draw curves. Let's take a look at the first type of curve: the quadratic bezier curve.`,
19 | creator: "@nandafyi",
20 | },
21 | openGraph: {
22 | title: "SVG Path Commands | Bezier Curves",
23 | description: `The real power of SVG paths lies in its ability to draw curves. Let's take a look at the first type of curve: the quadratic bezier curve.`,
24 | url: "https://nan.fyi/svg-paths",
25 | siteName: "Not a Number",
26 | },
27 | };
28 |
29 | export default async function BezierCurvesPage() {
30 | const { content, length } = await readPage("bezier-curves");
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/bezier-curves/state.ts:
--------------------------------------------------------------------------------
1 | import { useStateContext as useStateContextBase } from "../components/state-context";
2 | import { getInitialPracticeQuestionState } from "../components/path-practice";
3 | import { parsePath } from "../lib/path";
4 | import { createInitialState } from "../components/svg/drag-group";
5 |
6 | export const initialState = {
7 | intro: parsePath(
8 | "M 5 5 q 5 -3 10 0 M 5 10 c 5 -3 5 3 10 0 M 5 15 a 5 3 0 0 0 10 0"
9 | ),
10 | curve: {
11 | path: parsePath("M 5 0 v 5 Q 5 15 15 15 h 5"),
12 | ...createInitialState(),
13 | },
14 | chain: {
15 | path: parsePath("M 5 5 Q 5 10 10 10 T 15 15"),
16 | ...createInitialState(),
17 | },
18 | ...getInitialPracticeQuestionState(
19 | parsePath(
20 | "M 5 17 Q 10 8 15 17 M 10 12.5 Q 15 5 20 12.5 M 5 5 v 15 h 15 v -15 z"
21 | )
22 | ),
23 | };
24 |
25 | export const useStateContext = (
26 | key: Key
27 | ) => {
28 | return useStateContextBase()(key);
29 | };
30 |
--------------------------------------------------------------------------------
/app/svg-paths/challenge/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/challenge/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/challenge/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 | import { readPage } from "../lib/fs";
4 | import { Content } from "./content";
5 |
6 | export const metadata: Metadata = {
7 | title: "SVG Path Commands | Challenges",
8 | description: `Build a better intuition of SVG paths by tracing over eight beautiful icons courtesy of the Iconist's Central icon system.`,
9 | authors: [
10 | {
11 | name: "Nanda Syahrasyad",
12 | url: "https://twitter.com/nandafyi",
13 | },
14 | ],
15 | twitter: {
16 | card: "summary_large_image",
17 | title: "SVG Path Commands | Challenges",
18 | description: `Build a better intuition of SVG paths by tracing over eight beautiful icons courtesy of the Iconist's Central icon system.`,
19 | creator: "@nandafyi",
20 | },
21 | openGraph: {
22 | title: "SVG Path Commands | Challenges",
23 | description: `Build a better intuition of SVG paths by tracing over eight beautiful icons courtesy of the Iconist's Central icon system.`,
24 | url: "https://nan.fyi/svg-paths",
25 | siteName: "Not a Number",
26 | },
27 | };
28 |
29 | export default async function ChallengePage() {
30 | const { content, length } = await readPage("challenge");
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/components/button.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 | import { motion } from "framer-motion";
3 |
4 | export function Button({
5 | className,
6 | ...props
7 | }: React.ComponentPropsWithoutRef<(typeof motion)["button"]>) {
8 | return (
9 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/svg-paths/components/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | // src: https://usehooks-ts.com/react-hook/use-media-query
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | export function useMediaQuery(query: string): boolean {
6 | const getMatches = (query: string): boolean => {
7 | // Prevents SSR issues
8 | if (typeof window !== "undefined") {
9 | return window.matchMedia(query).matches;
10 | }
11 | return false;
12 | };
13 |
14 | const [matches, setMatches] = useState(getMatches(query));
15 |
16 | function handleChange() {
17 | setMatches(getMatches(query));
18 | }
19 |
20 | useEffect(() => {
21 | const matchMedia = window.matchMedia(query);
22 |
23 | // Triggered at the first client-side load and if query changes
24 | handleChange();
25 |
26 | // Listen matchMedia
27 | matchMedia.addEventListener("change", handleChange);
28 |
29 | return () => {
30 | matchMedia.removeEventListener("change", handleChange);
31 | };
32 | // eslint-disable-next-line react-hooks/exhaustive-deps
33 | }, [query]);
34 |
35 | return matches;
36 | }
37 |
--------------------------------------------------------------------------------
/app/svg-paths/components/icons.tsx:
--------------------------------------------------------------------------------
1 | export const Refresh = () => {
2 | return (
3 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export const Pause = () => {
22 | return (
23 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export const Play = () => {
40 | return (
41 |
51 |
52 |
53 | );
54 | };
55 |
56 | export const ArrowRight = () => {
57 | return (
58 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export const ArrowLeft = () => {
75 | return (
76 |
86 |
87 |
88 |
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/app/svg-paths/components/index-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | const IndexContext = React.createContext<{
6 | index: number;
7 | numSections: number;
8 | next: () => void;
9 | prev: () => void;
10 | set: (index: number) => void;
11 | }>(null);
12 |
13 | export function IndexProvider({
14 | children,
15 | numSections = Number.POSITIVE_INFINITY,
16 | }: {
17 | numSections?: number;
18 | children: React.ReactNode;
19 | }) {
20 | const [index, setIndex] = React.useState(0);
21 | const next = React.useCallback(
22 | () => setIndex((i) => Math.min(i + 1, numSections - 1)),
23 | [numSections]
24 | );
25 | const prev = React.useCallback(() => setIndex((i) => Math.max(i - 1, 0)), []);
26 | return (
27 |
30 | {children}
31 |
32 | );
33 | }
34 |
35 | export const useIndexContext = () => React.useContext(IndexContext);
36 |
--------------------------------------------------------------------------------
/app/svg-paths/components/interactive-command.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion, transform } from "framer-motion";
3 | import { useStateContext } from "./state-context";
4 |
5 | const BUTTON_GRID_SIZE = 32;
6 | const GRID_PADDING = 4;
7 | const MIDPOINT = BUTTON_GRID_SIZE / 2;
8 |
9 | const getOffset = transform([-100, 100], [-MIDPOINT, MIDPOINT]);
10 |
11 | export const InteractiveCommand = ({ id, template }) => {
12 | const [offset, setOffset] = React.useState({ x: 0, y: 0 });
13 | const {
14 | data: { x, y },
15 | set,
16 | } = useStateContext>()(id);
17 | return (
18 |
19 |
20 | {template({ x, y })}
21 |
22 |
{
25 | const deltaX = transform(info.delta.x, [-100, 100], [-10, 10]);
26 | const deltaY = transform(info.delta.y, [-100, 100], [-10, 10]);
27 | set({ x: x + deltaX, y: y + deltaY });
28 | setOffset({
29 | x: getOffset(info.offset.x),
30 | y: getOffset(info.offset.y),
31 | });
32 | }}
33 | onPanEnd={() => setOffset({ x: 0, y: 0 })}
34 | >
35 |
42 |
48 |
49 |
50 |
51 |
52 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/app/svg-paths/components/page-section.module.css:
--------------------------------------------------------------------------------
1 | .section > * {
2 | grid-column: 2;
3 | }
4 |
5 | .section > figure,
6 | .section > svg {
7 | grid-column: 1 / -1;
8 | }
9 |
10 | .prefix + .section:first-of-type {
11 | padding-top: 0;
12 | }
13 |
14 | @media (min-width: theme(screens.lg)) {
15 | .section > figure,
16 | .section > svg {
17 | display: none;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/svg-paths/components/path-editor.tsx:
--------------------------------------------------------------------------------
1 | import { useStateContext } from "./state-context";
2 |
3 | export const PathEditor = ({ id, placeholder = "" }) => {
4 | const { data, set } =
5 | useStateContext>()(id);
6 | const value = data?.value || "";
7 | return (
8 |
9 |
10 |
11 |
12 |
19 | );
20 | };
21 |
22 | const EditIcon = () => {
23 | return (
24 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/app/svg-paths/components/path-hover-visual.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { AbsoluteCommand, type Path } from "../lib/path";
3 | import { PathSection, PathSectionPoint } from "./path-visualizer";
4 | import { useStateContext } from "./state-context";
5 | import { useSvgContext } from "./svg";
6 |
7 | export function PathHoverVisual({ source }: { source: string }) {
8 | const { data } =
9 | useStateContext>()(source);
10 | return ;
11 | }
12 |
13 | export function PathList({
14 | commands,
15 | index,
16 | }: {
17 | commands: AbsoluteCommand[];
18 | index?: number;
19 | }) {
20 | const { useRelativeMotionValue } = useSvgContext();
21 | const isHovering = typeof index === "number";
22 | const activeCommand = commands[index];
23 | const circleR = useRelativeMotionValue(0.75);
24 | return (
25 | <>
26 |
27 | {commands.map((command) => {
28 | const getClassName = () => {
29 | if (command.code !== "M") return;
30 | return isHovering ? "stroke-gray8" : "stroke-gray10";
31 | };
32 | return (
33 |
34 |
39 |
40 | );
41 | })}
42 | {commands.map((command) => {
43 | return (
44 |
45 |
46 |
47 | );
48 | })}
49 |
50 | {activeCommand && (
51 |
52 |
58 |
59 |
60 |
61 | )}
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/app/svg-paths/components/path.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useSvgContext } from "./svg";
3 |
4 | export function Path(
5 | props: React.ComponentPropsWithoutRef<(typeof motion)["path"]>
6 | ) {
7 | const { useRelativeMotionValue } = useSvgContext();
8 | return (
9 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/app/svg-paths/components/play-slider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useInterval from "@use-it/interval";
3 | import { Button } from "./button";
4 | import { Slider } from "./slider";
5 | import { useStateContext } from "./state-context";
6 |
7 | export function PlaySliderFromSource({ source }: { source: string }) {
8 | const {
9 | data: { index, maxIndex },
10 | set,
11 | } =
12 | useStateContext<
13 | Record
14 | >()(source);
15 | return (
16 | set({ index: newIndex })}
20 | />
21 | );
22 | }
23 |
24 | export function PlaySlider({
25 | index,
26 | maxIndex,
27 | onChange,
28 | }: {
29 | index: number | null;
30 | maxIndex: number;
31 | onChange: (index: number) => void;
32 | }) {
33 | const [isPlaying, setIsPlaying] = React.useState(false);
34 |
35 | useInterval(
36 | () => {
37 | if (index === maxIndex - 1) {
38 | return setIsPlaying(false);
39 | }
40 | onChange(index === null ? 0 : index + 1);
41 | },
42 | isPlaying ? 500 : null
43 | );
44 |
45 | const play = () => {
46 | if (index === maxIndex - 1) {
47 | onChange(null);
48 | }
49 | setIsPlaying(!isPlaying);
50 | };
51 |
52 | return (
53 |
54 | {isPlaying ? "Pause" : "Play"}
55 | onChange(i ? i - 1 : null)}
58 | min={0}
59 | max={maxIndex}
60 | />
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/app/svg-paths/components/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 | import { clsx as cn } from "clsx";
6 |
7 | const Popover = PopoverPrimitive.Root;
8 |
9 | const PopoverTrigger = PopoverPrimitive.Trigger;
10 |
11 | const PopoverContent = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, align = "center", sideOffset = 8, ...props }, ref) => (
15 |
16 |
26 |
27 | ));
28 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
29 |
30 | export { Popover, PopoverTrigger, PopoverContent };
31 |
--------------------------------------------------------------------------------
/app/svg-paths/components/ripple.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { useSvgContext } from "./svg";
4 |
5 | export function Ripple({
6 | cx,
7 | cy,
8 | delay = 0,
9 | color = "fill-gray12",
10 | children,
11 | }: {
12 | cx: number;
13 | cy: number;
14 | delay?: number;
15 | color?: string;
16 | children: React.ReactNode;
17 | }) {
18 | const id = React.useId();
19 | const { config, getRelative } = useSvgContext();
20 | const padding = getRelative(config.padding);
21 | const endpointSize = getRelative(1);
22 | return (
23 |
24 |
25 |
32 |
41 |
42 |
56 |
65 | {children}
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/app/svg-paths/components/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SliderPrimitive from "@radix-ui/react-slider";
3 | import { clsx } from "clsx";
4 |
5 | const Slider = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 |
18 |
19 |
20 |
21 |
22 | ));
23 | Slider.displayName = SliderPrimitive.Root.displayName;
24 |
25 | export { Slider };
26 |
--------------------------------------------------------------------------------
/app/svg-paths/components/state-context.tsx:
--------------------------------------------------------------------------------
1 | import produce from "immer";
2 | import React from "react";
3 |
4 | const StateContext = React.createContext<{
5 | state: Record;
6 | setState: React.Dispatch>>;
7 | }>(null);
8 |
9 | export function StateProvider>({
10 | initial,
11 | children,
12 | }: {
13 | initial: StateType;
14 | children: React.ReactNode;
15 | }) {
16 | const [state, setState] = React.useState(initial);
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
24 | export function useStateContext>() {
25 | const { state, setState } = React.useContext(StateContext);
26 | const _state = state as StateType;
27 | return (key: Key) => {
28 | const data = _state[key] as StateType[Key];
29 | const set = (
30 | data: StateType[Key] extends object
31 | ? Partial
32 | : StateType[Key]
33 | ) => {
34 | setState((current) =>
35 | produce(current, (draft: StateType) => {
36 | if (data === null) {
37 | draft[key] = null;
38 | return;
39 | }
40 | if (typeof data === "object") {
41 | const current = draft[key] as object;
42 | const update = data as Partial;
43 | draft[key] = { ...current, ...update } as StateType[Key];
44 | return;
45 | }
46 | draft[key] = data as StateType[Key];
47 | })
48 | );
49 | };
50 | return { data, set };
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/app/svg-paths/components/svg/circle.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { useSvgContext } from "../svg";
4 |
5 | type CircleSize = "base" | "small" | "large";
6 |
7 | type CircleVariant = "primary" | "secondary" | "cursor" | "point" | "highlight";
8 |
9 | const mapSizeToRadius = {
10 | small: 0.75,
11 | base: 1,
12 | large: 1.25,
13 | };
14 |
15 | const mapVariantToFill = {
16 | primary: "fill-gray10",
17 | secondary: "fill-gray8",
18 | cursor: "fill-current",
19 | point: "fill-gray4",
20 | };
21 |
22 | export function Circle({
23 | variant = "primary",
24 | size = "base",
25 | ...props
26 | }: {
27 | variant?: CircleVariant;
28 | size?: CircleSize;
29 | } & React.ComponentPropsWithoutRef<(typeof motion)["circle"]>) {
30 | const { useRelativeMotionValue } = useSvgContext();
31 | const sizeValue = useRelativeMotionValue(mapSizeToRadius[size]);
32 | const strokeWidth = useRelativeMotionValue(variant === "point" ? 0.6 : 0);
33 | return (
34 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/svg-paths/components/svg/drag-group.tsx:
--------------------------------------------------------------------------------
1 | export type PointsGroup = Array<[number, number]>;
2 |
3 | export type DragGroupState = {
4 | state: "idle" | "hovering" | "panning";
5 | active: string[];
6 | };
7 |
8 | export function getDragHandlers({
9 | id,
10 | state,
11 | set,
12 | }: {
13 | id: string[];
14 | state: DragGroupState["state"];
15 | set: (state: DragGroupState) => void;
16 | }) {
17 | return {
18 | onHoverStart: () => {
19 | if (state !== "idle") return;
20 | set({ state: "hovering", active: id });
21 | },
22 | onHoverEnd: () => {
23 | if (state !== "hovering") return;
24 | set({ state: "idle", active: null });
25 | },
26 | onPanStart: () => {
27 | set({ state: "panning", active: id });
28 | },
29 | onPanEnd: () => {
30 | set({ state: "idle", active: null });
31 | },
32 | };
33 | }
34 |
35 | export function createInitialState(): DragGroupState {
36 | return {
37 | state: "idle",
38 | active: null,
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/app/svg-paths/components/svg/line.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { useSvgContext } from "../svg";
4 |
5 | type LineVariant = "primary" | "secondary" | "current" | "none";
6 |
7 | export function Line({
8 | variant = "primary",
9 | dashed = false,
10 | size = "base",
11 | ...props
12 | }: {
13 | variant?: LineVariant;
14 | dashed?: boolean;
15 | size?: "base" | "xl";
16 | } & React.ComponentPropsWithoutRef<(typeof motion)["line"]>) {
17 | const { useRelativeMotionValue } = useSvgContext();
18 | const dashedValue = useRelativeMotionValue(1);
19 | const mapVariantToStroke = {
20 | primary: "stroke-gray10",
21 | secondary: "stroke-gray8",
22 | current: "stroke-current",
23 | none: undefined,
24 | };
25 | const mapSizeToWidth = {
26 | base: 0.5,
27 | xl: 1.25,
28 | };
29 | return (
30 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/svg-paths/components/svg/path.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { useSvgContext } from "../svg";
4 |
5 | export function Path(
6 | props: React.ComponentPropsWithoutRef<(typeof motion)["path"]>
7 | ) {
8 | const { useRelativeMotionValue } = useSvgContext();
9 | return (
10 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/app/svg-paths/components/svg/text.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { useMediaQuery } from "../hooks/use-media-query";
3 | import { useSvgContext } from "../svg";
4 |
5 | type TextProps = Omit, "fontSize"> & {
6 | font?: string;
7 | fontSize?: number;
8 | variant?: "current" | "secondary" | "primary";
9 | };
10 |
11 | const mapVariantToFill = {
12 | current: "fill-current",
13 | secondary: "fill-gray10",
14 | primary: "fill-gray12",
15 | };
16 |
17 | export const Text = ({
18 | children,
19 | fontSize = 2,
20 | font = "font-mono",
21 | variant,
22 | ...props
23 | }: TextProps) => {
24 | const isMobile = useMediaQuery("(max-width: 768px)");
25 | const { getRelative } = useSvgContext();
26 | const _fontSize = getRelative(isMobile ? fontSize * 2 : fontSize);
27 | return (
28 |
35 |
36 | {children}
37 |
38 |
42 | {children}
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/app/svg-paths/components/visual-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useIndexContext } from "./index-provider";
2 | import { type Config, Svg } from "./svg";
3 |
4 | export type Page = {
5 | children: React.ReactNode;
6 | svg?:
7 | | false
8 | | number
9 | | {
10 | size?: number;
11 | config?: Partial;
12 | };
13 | };
14 |
15 | export type VisualWrapperComponents = Page[];
16 |
17 | export function VisualWrapper({
18 | components,
19 | }: {
20 | components: VisualWrapperComponents;
21 | }) {
22 | const { index } = useIndexContext();
23 | const component = components[index];
24 | if (!component) return ;
25 |
26 | const { children, svg = 25 } = component;
27 | if (!svg) return <>{children}>;
28 |
29 | let props = svg;
30 | if (typeof props === "number") props = { size: props };
31 | return {children} ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/content.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { MDX } from "../components/mdx";
5 | import { StateProvider } from "../components/state-context";
6 | import { VisualWrapper } from "../components/visual-wrapper";
7 | import { PracticeQuestion } from "../components/path-practice";
8 | import * as syntax from "./content/syntax";
9 | import * as curveGeneral from "./content/curve-general";
10 | import * as chain from "./content/chain";
11 | import * as pill from "./content/pill";
12 | import { initialState } from "./state";
13 |
14 | export function Content({ content, length }) {
15 | return (
16 |
17 |
28 | ,
36 | svg: 25,
37 | },
38 | ]}
39 | />
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/content/curve-general.tsx:
--------------------------------------------------------------------------------
1 | import { CubicPlayground } from "./cubic-playground";
2 | import { useStateContext } from "../state";
3 | import { Path } from "app/svg-paths/components/svg/path";
4 | import { Circle } from "app/svg-paths/components/svg/circle";
5 | import { Line } from "app/svg-paths/components/svg/line";
6 |
7 | function CurveGeneral() {
8 | const { data, set } = useStateContext("syntax");
9 | return (
10 |
11 |
12 |
13 |
14 |
22 |
29 |
30 | {
33 | set({
34 | path: data.path.setAbsolute(1, curve),
35 | });
36 | }}
37 | />
38 |
39 | );
40 | }
41 |
42 | export const components = { CurveGeneral };
43 |
44 | export const page = {
45 | children: ,
46 | svg: 20,
47 | };
48 |
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/content/syntax.tsx:
--------------------------------------------------------------------------------
1 | import { useStateContext } from "../state";
2 | import { CubicPlayground } from "./cubic-playground";
3 |
4 | function Syntax() {
5 | const { data, set } = useStateContext("syntax");
6 | return (
7 | {
11 | set({
12 | path: data.path.setAbsolute(1, curve),
13 | ...changes,
14 | });
15 | }}
16 | tooltip
17 | />
18 | );
19 | }
20 |
21 | export const components = { Syntax };
22 |
23 | export const page = {
24 | children: ,
25 | svg: 20,
26 | };
27 |
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/cubic-curves/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 | import { readPage } from "../lib/fs";
4 | import { Content } from "./content";
5 |
6 | export const metadata: Metadata = {
7 | title: "SVG Path Commands | Cubic Curves",
8 | description: `The cubic bezier curve is a curve type that lets you draw curves with more control. How does it work, and how does it differ from the bezier curves we just saw?`,
9 | authors: [
10 | {
11 | name: "Nanda Syahrasyad",
12 | url: "https://twitter.com/nandafyi",
13 | },
14 | ],
15 | twitter: {
16 | card: "summary_large_image",
17 | title: "SVG Path Commands | Cubic Curves",
18 | description: `The cubic bezier curve is a curve type that lets you draw curves with more control. How does it work, and how does it differ from the bezier curves we just saw?`,
19 | creator: "@nandafyi",
20 | },
21 | openGraph: {
22 | title: "SVG Path Commands | Cubic Curves",
23 | description: `The cubic bezier curve is a curve type that lets you draw curves with more control. How does it work, and how does it differ from the bezier curves we just saw?`,
24 | url: "https://nan.fyi/svg-paths",
25 | siteName: "Not a Number",
26 | },
27 | };
28 |
29 | export default async function CubicCurvesPage() {
30 | const { content, length } = await readPage("cubic-curves");
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/state.ts:
--------------------------------------------------------------------------------
1 | import { useStateContext as useStateContextBase } from "../components/state-context";
2 | import { getInitialPracticeQuestionState } from "../components/path-practice";
3 | import { parsePath } from "../lib/path";
4 | import { createInitialState } from "../components/svg/drag-group";
5 |
6 | export const initialState = {
7 | syntax: {
8 | path: parsePath("M 5 13 C 0 5 20 5 15 13"),
9 | ...createInitialState(),
10 | },
11 | chain: {
12 | path: parsePath(
13 | "M 6 10 C 6 8 4 10 4 5 S 11 0 11 5 S 9 8 9 10 Z M 6.25 10 v 1.5 h -0.25 v 1 q 0 1 1 1 h 1 q 1 0 1 -1 v -1 h -3 m 2.75 0 v -1.5"
14 | ),
15 | index: null,
16 | expanded: false,
17 | highlight: [1, 2, 3],
18 | collapseAfter: 4,
19 | ...createInitialState(),
20 | },
21 | ...getInitialPracticeQuestionState(
22 | parsePath(
23 | "M 10 18 c -10 -1 -10 -14 0 -15 q 6 -0.75 8 5 c 7 0 7 10 0 10 m -1.0 -2 v 6 m -2.0 -4.5 v 6 m -2.0 -6 v 6 m -2.0 -7.5 v 6"
24 | )
25 | ),
26 | };
27 |
28 | export const useStateContext = (
29 | key: Key
30 | ) => {
31 | return useStateContextBase()(key);
32 | };
33 |
--------------------------------------------------------------------------------
/app/svg-paths/cubic-curves/types.ts:
--------------------------------------------------------------------------------
1 | export type SyntaxState = {
2 | x1: number;
3 | y1: number;
4 | x2: number;
5 | y2: number;
6 | x: number;
7 | y: number;
8 | active: "x1" | "x2" | "x" | null;
9 | state: "idle" | "hovering" | "panning";
10 | };
11 |
--------------------------------------------------------------------------------
/app/svg-paths/cursors/content.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MDX } from "../components/mdx";
4 | import {
5 | Cursors,
6 | CursorOverview,
7 | Practice,
8 | Corner,
9 | AbsoluteRelative,
10 | } from "./cursors";
11 | import { StateProvider } from "../components/state-context";
12 | import { initialState } from "./state";
13 |
14 | export function CursorsContent({ content, length }) {
15 | return (
16 |
17 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/svg-paths/cursors/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/cursors/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/cursors/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 | import { readPage } from "../lib/fs";
4 | import { CursorsContent } from "./content";
5 |
6 | export const metadata: Metadata = {
7 | title: "SVG Path Commands | Cursors",
8 | description: `The cursor is the "pen" that draws the path - but how does it work, and how does each command affect it?`,
9 | authors: [
10 | {
11 | name: "Nanda Syahrasyad",
12 | url: "https://twitter.com/nandafyi",
13 | },
14 | ],
15 | twitter: {
16 | card: "summary_large_image",
17 | title: "SVG Path Commands | Cursors",
18 | description: `The cursor is the "pen" that draws the path - but how does it work, and how does each command affect it?`,
19 | creator: "@nandafyi",
20 | },
21 | openGraph: {
22 | title: "SVG Path Commands | Cursors",
23 | description: `The cursor is the "pen" that draws the path - but how does it work, and how does each command affect it?`,
24 | url: "https://nan.fyi/svg-paths",
25 | siteName: "Not a Number",
26 | },
27 | };
28 |
29 | export default async function CursorsPage() {
30 | const { content, length } = await readPage("cursors");
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/cursors/state.ts:
--------------------------------------------------------------------------------
1 | import { useStateContext as useStateContextBase } from "../components/state-context";
2 | import { getInitialPracticeQuestionState } from "../components/path-practice";
3 | import { parsePath } from "../lib/path";
4 | import { createInitialState } from "../components/svg/drag-group";
5 |
6 | const heart = `M11.995 7.23319
7 | C10.5455 5.60999 8.12832 5.17335 6.31215 6.65972
8 | C4.4959 8.14609 4.2403 10.6312 5.66654 12.3892
9 | L11.995 18.25
10 | L18.3235 12.3892
11 | C19.7498 10.6312 19.5253 8.13046 17.6779 6.65972
12 | C15.8305 5.18899 13.4446 5.60999 11.995 7.23319
13 | Z`;
14 |
15 | const heartPath = parsePath(heart);
16 |
17 | const cornerPath = parsePath("M 5 5 v 5 L 10 15 h 5");
18 |
19 | export const initialState = {
20 | intro: {
21 | path: heartPath,
22 | index: null,
23 | maxIndex: heartPath.commands.length,
24 | },
25 | corner: {
26 | path: cornerPath,
27 | index: null,
28 | maxIndex: cornerPath.commands.length,
29 | },
30 | absolute: {
31 | path: parsePath("M 5 5 L 10 15"),
32 | ...createInitialState(),
33 | },
34 | relative: {
35 | path: parsePath("M 15 5 l 10 15"),
36 | ...createInitialState(),
37 | },
38 | move: {
39 | path: parsePath("M 3 5.5 q 2 2 0 4 m 3 -6 q 4 4 0 8 m 3 -10 q 4 6 0 12"),
40 | },
41 | ...getInitialPracticeQuestionState(
42 | parsePath("M 0 5 m 5 10 m 5 -5 m 5 0 m 5 -10")
43 | ),
44 | };
45 |
46 | export const useStateContext = (
47 | key: Key
48 | ) => {
49 | return useStateContextBase()(key);
50 | };
51 |
--------------------------------------------------------------------------------
/app/svg-paths/index/absolute.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from "../components/svg/text";
2 | import { Circle } from "../components/svg/circle";
3 | import { Line } from "../components/svg/line";
4 | import { CoordinatesTooltip } from "../components/svg/tooltip";
5 | import { Page } from "../components/visual-wrapper";
6 | import { useStateContext } from "./state";
7 |
8 | export function Absolute() {
9 | const {
10 | data: { path },
11 | } = useStateContext("absolute");
12 | const line = path.atAbsolute(1);
13 | const relativeLine = path.atAbsolute(3);
14 | return (
15 |
16 |
17 |
25 |
26 |
27 | L 5 5
28 |
29 |
30 |
31 |
39 |
40 |
41 | l 5 5
42 |
43 |
44 |
45 |
46 |
51 |
52 | );
53 | }
54 |
55 | export const page: Page = {
56 | svg: 20,
57 | children: ,
58 | };
59 |
--------------------------------------------------------------------------------
/app/svg-paths/index/commands.tsx:
--------------------------------------------------------------------------------
1 | import { Circle } from "../components/svg/circle";
2 | import { Path } from "../components/svg/path";
3 | import { createPath } from "../lib/path";
4 | import { useStateContext } from "./state";
5 |
6 | export function Commands() {
7 | const {
8 | data: { path, index },
9 | } = useStateContext("commands");
10 | const last = index === null ? path.commands.length : index + 1;
11 | return (
12 |
13 |
17 |
18 | {path.absolute.slice(0, last).map((command) => {
19 | return (
20 |
27 | );
28 | })}
29 |
30 |
31 | {path.absolute.map((command) => {
32 | return (
33 |
39 | );
40 | })}
41 |
42 |
43 | );
44 | }
45 |
46 | export const page = {
47 | svg: 25,
48 | children: ,
49 | };
50 |
--------------------------------------------------------------------------------
/app/svg-paths/index/content.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Button } from "../components/button";
5 | import { MDX } from "../components/mdx";
6 | import { StateProvider } from "../components/state-context";
7 | import { VisualWrapper } from "../components/visual-wrapper";
8 | import { initialState, useStateContext } from "./state";
9 | import * as heart from "./heart";
10 | import * as square from "./square";
11 | import * as commands from "./commands";
12 | import * as absolute from "./absolute";
13 |
14 | export function Content({ content, length }) {
15 | return (
16 |
17 |
22 |
23 | Understanding SVG Paths
24 |
25 |
26 | If you've ever looked at the SVG code for an icon before, you
27 | might have noticed that they're usually made up of a bunch of{" "}
28 | path
{" "}
29 | elements, each with a cryptic{" "}
30 | d
{" "}
31 | attribute.
32 |
33 | >
34 | }
35 | components={{
36 | SquareToggle,
37 | Square: square.Square,
38 | Commands: commands.Commands,
39 | Absolute: absolute.Absolute,
40 | Heart: heart.Heart,
41 | }}
42 | >
43 |
52 |
53 |
54 | );
55 | }
56 |
57 | function SquareToggle() {
58 | const { data, set } = useStateContext("intro");
59 | return (
60 | set({ toggled: !data.toggled })}
63 | >
64 | {data.toggled ? "Straighten!" : "Bend!"}
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/app/svg-paths/index/heart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PathVisualizer } from "../components/path-visualizer";
4 | import type { Page } from "../components/visual-wrapper";
5 |
6 | export const heart = `M11.995 7.23319
7 | C10.5455 5.60999 8.12832 5.17335 6.31215 6.65972
8 | C4.4959 8.14609 4.2403 10.6312 5.66654 12.3892
9 | L11.995 18.25
10 | L18.3235 12.3892
11 | C19.7498 10.6312 19.5253 8.13046 17.6779 6.65972
12 | C15.8305 5.18899 13.4446 5.60999 11.995 7.23319
13 | Z`;
14 |
15 | type HeartType = "cursor" | "line" | "curve";
16 |
17 | export function Heart({ type }: { type?: HeartType }) {
18 | return ;
19 | }
20 |
21 | export const page = (type?: HeartType): Page => {
22 | return {
23 | svg: 25,
24 | children: ,
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/app/svg-paths/index/state.ts:
--------------------------------------------------------------------------------
1 | import { useStateContext as useStateContextBase } from "../components/state-context";
2 | import { parsePath } from "../lib/path";
3 |
4 | const heart = `M11.995 7.23319
5 | C10.5455 5.60999 8.12832 5.17335 6.31215 6.65972
6 | C4.4959 8.14609 4.2403 10.6312 5.66654 12.3892
7 | L11.995 18.25
8 | L18.3235 12.3892
9 | C19.7498 10.6312 19.5253 8.13046 17.6779 6.65972
10 | C15.8305 5.18899 13.4446 5.60999 11.995 7.23319
11 | Z`;
12 |
13 | const heartPath = parsePath(heart);
14 |
15 | export const initialState = {
16 | intro: {
17 | toggled: false,
18 | },
19 | commands: {
20 | path: heartPath,
21 | index: null,
22 | maxIndex: heartPath.commands.length,
23 | },
24 | absolute: {
25 | path: parsePath("M 10 10 L 5 5 M 10 10 l 5 5"),
26 | index: null,
27 | highlight: [1, 3],
28 | },
29 | };
30 |
31 | export const useStateContext = (
32 | key: Key
33 | ) => {
34 | return useStateContextBase()(key);
35 | };
36 |
--------------------------------------------------------------------------------
/app/svg-paths/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function Layout({ children }: { children: React.ReactNode }) {
2 | return (
3 |
4 |
5 | {children}
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/svg-paths/lib/fs.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs/promises";
2 |
3 | export const readPage = async (path: string) => {
4 | const source = await fs.readFile(
5 | `${process.cwd()}/app/svg-paths/${path}/index.mdx`,
6 | "utf8"
7 | );
8 | return splitPage(source);
9 | };
10 |
11 | /**
12 | * Wraps each section of the page in a PageSection component. A section is
13 | * delimited by "---".
14 | */
15 | const splitPage = (page: string) => {
16 | const sections = page.split("---");
17 | const parts = sections.flatMap((section, index) => {
18 | if (index === sections.length - 1) return [section];
19 | return [section, `\n\n`];
20 | });
21 | parts.unshift("\n\n");
22 | parts.push("\n ");
23 | return {
24 | content: parts.join(""),
25 | length: sections.length,
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/app/svg-paths/lines/components.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { Button } from "../components/button";
4 | import { useStateContext } from "./state";
5 |
6 | export function HeartCommands() {
7 | return (
8 |
9 |
10 | M 11.995 7.23319
11 |
12 | C 10.5455 5.60999 8.12832 5.17335 6.31215 6.65972
13 | C 4.4959 8.14609 4.2403 10.6312 5.66654 12.3892
14 | L 11.995 18.25
15 | L 18.3235 12.3892
16 | C 19.7498 10.6312 19.5253 8.13046 17.6779 6.65972
17 |
18 | C 15.8305 5.18899 13.4446 5.60999{" "}
19 | 11.995 7.23319
20 |
21 |
22 | );
23 | }
24 |
25 | export function ClosePathToggle() {
26 | const { data, set } = useStateContext("z");
27 | return (
28 |
29 |
set({ active: !data.active })}>Toggle Z
30 |
31 | M 11.995 7.23319
32 | C 10.5455 5.60999 8.12832 5.17335 6.31215 6.65972
33 | C 4.4959 8.14609 4.2403 10.6312 5.66654 12.3892
34 | L 11.995 18.25
35 | L 18.3235 12.3892
36 | C 19.7498 10.6312 19.5253 8.13046 17.6779 6.65972
37 | C 15.8305 5.18899 13.4446 5.60999 11.995 7.23319
38 | {data.active && (
39 |
44 | Z
45 |
46 | )}
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/svg-paths/lines/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/lines/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/lines/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 | import { readPage } from "../lib/fs";
4 | import { LinesContent } from "./content";
5 |
6 | export const metadata: Metadata = {
7 | title: "SVG Path Commands | Lines",
8 | description: `The L, or line, command is the simplest command out of all the path commands. Let's take a look at how it works.`,
9 | authors: [
10 | {
11 | name: "Nanda Syahrasyad",
12 | url: "https://twitter.com/nandafyi",
13 | },
14 | ],
15 | twitter: {
16 | card: "summary_large_image",
17 | title: "SVG Path Commands | Lines",
18 | description: `The L, or line, command is the simplest command out of all the path commands. Let's take a look at how it works.`,
19 | creator: "@nandafyi",
20 | },
21 | openGraph: {
22 | title: "SVG Path Commands | Lines",
23 | description: `The L, or line, command is the simplest command out of all the path commands. Let's take a look at how it works.`,
24 | url: "https://nan.fyi/svg-paths",
25 | siteName: "Not a Number",
26 | },
27 | };
28 |
29 | export default async function LinesPage() {
30 | const { content, length } = await readPage("lines");
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/app/svg-paths/lines/state.ts:
--------------------------------------------------------------------------------
1 | import { useStateContext as useStateContextBase } from "../components/state-context";
2 | import { getInitialPracticeQuestionState } from "../components/path-practice";
3 | import { parsePath } from "../lib/path";
4 | import { createInitialState } from "../components/svg/drag-group";
5 |
6 | export const initialState = {
7 | absolute: {
8 | path: parsePath("M 5 5 L 15 10"),
9 | ...createInitialState(),
10 | },
11 | relative: {
12 | path: parsePath("M 5 5 l 15 10"),
13 | ...createInitialState(),
14 | },
15 | vertical: {
16 | path: parsePath("M 13 5 h -6 V 15 H 13 M 7 10 h 4"),
17 | },
18 | closePath: {
19 | path: parsePath("M 10 5 l -5 10 h 10 Z"),
20 | },
21 | z: { active: false },
22 | ...getInitialPracticeQuestionState(
23 | parsePath(
24 | "M 5 10 l 2.5 -5 h 10 l 2.5 5 h -12.5 v 10 h 5 v -5 h -2.5 v 5 h 7.5 v -10 z"
25 | )
26 | ),
27 | };
28 |
29 | export const useStateContext = (
30 | key: Key
31 | ) => {
32 | return useStateContextBase()(key);
33 | };
34 |
--------------------------------------------------------------------------------
/app/svg-paths/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/app/svg-paths/opengraph-image.png
--------------------------------------------------------------------------------
/app/svg-paths/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { serialize } from "next-mdx-remote/serialize";
3 |
4 | import { Content } from "./index/content";
5 | import { readPage } from "./lib/fs";
6 |
7 | export const metadata: Metadata = {
8 | title: "A Deep Dive Into SVG Path Commands",
9 | description:
10 | "An interactive guide to understanding SVG paths and path commands.",
11 | authors: [
12 | {
13 | name: "Nanda Syahrasyad",
14 | url: "https://twitter.com/nandafyi",
15 | },
16 | ],
17 | twitter: {
18 | card: "summary_large_image",
19 | title: "A Deep Dive Into SVG Path Commands",
20 | description:
21 | "An interactive guide to understanding SVG paths and path commands.",
22 | creator: "@nandafyi",
23 | },
24 | openGraph: {
25 | title: "A Deep Dive Into SVG Path Commands",
26 | description:
27 | "An interactive guide to understanding SVG paths and path commands.",
28 | url: "https://nan.fyi/svg-paths",
29 | siteName: "Not a Number",
30 | },
31 | };
32 |
33 | export default async function SvgPathsPage() {
34 | const { content, length } = await readPage("index");
35 | return ;
36 | }
37 |
--------------------------------------------------------------------------------
/app/svg-paths/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | interface User {
6 | name: string;
7 | email: string;
8 | username: string;
9 | image: string;
10 | }
11 |
12 | interface Session {
13 | user: User;
14 | expires: string;
15 | access_token: string;
16 | }
17 |
18 | const AuthContext = React.createContext(null);
19 |
20 | export const useSession = (): Session | undefined => {
21 | return React.useContext(AuthContext);
22 | };
23 |
24 | export const AuthProvider = ({ children }) => {
25 | const [session, setSession] = React.useState();
26 |
27 | React.useEffect(() => {
28 | const fetchSession = async () => {
29 | const res = await fetch("/api/auth/session");
30 | const json = await res.json();
31 | setSession(json);
32 | };
33 | fetchSession();
34 | }, []);
35 |
36 | return (
37 | {children}
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/bin/component.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs/promises");
2 |
3 | const storiesTemplate = (
4 | componentName
5 | ) => `import { ${componentName} } from './${componentName}'
6 |
7 | export const Default = () => <${componentName} />
8 | `;
9 |
10 | const componentTemplate = (componentName) =>
11 | `export const ${componentName} = () => { return null; }`;
12 |
13 | const indexTemplate = (componentName) => `export * from './${componentName}'`;
14 |
15 | const generate = async (componentName, folder) => {
16 | const componentFolder = `${folder}/${componentName}`;
17 | await fs.mkdir(componentFolder);
18 | await Promise.all([
19 | fs.writeFile(
20 | `${componentFolder}/${componentName}.stories.tsx`,
21 | storiesTemplate(componentName)
22 | ),
23 | fs.writeFile(
24 | `${componentFolder}/${componentName}.tsx`,
25 | componentTemplate(componentName)
26 | ),
27 | fs.writeFile(`${componentFolder}/index.tsx`, indexTemplate(componentName)),
28 | ]);
29 | };
30 |
31 | generate(process.argv[2], process.argv[3]);
32 |
--------------------------------------------------------------------------------
/bin/stories.js:
--------------------------------------------------------------------------------
1 | const glob = require("glob");
2 | const fs = require("fs");
3 |
4 | const getStories = () => {
5 | return new Promise((resolve, reject) => {
6 | glob("**/*.stories.tsx", (err, files) => {
7 | if (err) {
8 | reject(err);
9 | }
10 | resolve(files);
11 | });
12 | });
13 | };
14 |
15 | const parseStoryPath = (storyPath) => {
16 | const parts = storyPath.split("/");
17 | const name = parts[parts.length - 1].replace(".stories.tsx", "");
18 |
19 | /**
20 | * My components that are custom for specific posts live in a separate directory from
21 | * shared components and are sometimes generated by Babel. This part changes the import
22 | * to come from the build directory instead of the source.
23 | */
24 | let postName;
25 | const isContentComponent = parts[0] === "content";
26 | if (isContentComponent) {
27 | parts[0] = "_dist-content";
28 | postName = parts[1];
29 | }
30 |
31 | /**
32 | * The source file is a TS file, but sometimes the output is a JS file, so we
33 | * explicitly remove the extension here.
34 | */
35 | parts[parts.length - 1] = `${name}.stories`;
36 | const path = parts.join("/");
37 |
38 | return {
39 | name,
40 | isContentComponent,
41 | path,
42 | postName,
43 | groupName: isContentComponent ? postName : parts[0],
44 | asImport: `import * as ${name} from './${path}'`,
45 | };
46 | };
47 |
48 | const toGroups = (stories) => {
49 | const groups = {};
50 | stories.forEach((story) => {
51 | const { postName, name, groupName } = story;
52 | if (!groups[groupName]) {
53 | groups[groupName] = [];
54 | }
55 | groups[groupName].push(name);
56 | });
57 | return groups;
58 | };
59 |
60 | const main = async () => {
61 | const files = (await getStories()).map(parseStoryPath);
62 | const groups = toGroups(files);
63 |
64 | console.log(groups);
65 |
66 | const imports = files.map(({ asImport }) => asImport).join(`\n`);
67 | const storiesProp = Object.entries(groups)
68 | .map(
69 | ([groupName, stories]) =>
70 | `{ name: \`${groupName}\`, stories: [${stories.map(
71 | (story) => `{ name: '${story}', variants: ${story} }`
72 | )}] }`
73 | )
74 | .join(`,\n`);
75 |
76 | fs.writeFileSync(
77 | "./stories.meta.ts",
78 | `${imports}\nexport const stories = [${storiesProp}];`
79 | );
80 | };
81 |
82 | main();
83 |
--------------------------------------------------------------------------------
/components/AlgorithmControls.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 | import type { AlgorithmContext } from "~/lib/algorithm/types";
3 |
4 | import { Slider } from "./Slider";
5 | import { PlayButton } from "./PlayButton";
6 | import { Row } from "./layout/Row";
7 |
8 | type AlgorithmControlsProps = {
9 | context: AlgorithmContext;
10 | };
11 |
12 | export const AlgorithmControls = ({ context }: AlgorithmControlsProps) => {
13 | return (
14 |
15 |
16 | context.goTo(step)}
21 | />
22 |
23 | {context.currentStep + 1}
24 | /
25 | {context.totalSteps}
26 |
27 |
28 | );
29 | };
30 |
31 | const Controls = styled(Row, {
32 | fontFamily: "$mono",
33 | gap: "$4",
34 | });
35 |
--------------------------------------------------------------------------------
/components/Callout.tsx:
--------------------------------------------------------------------------------
1 | import { FaPlus, FaMinus } from "react-icons/fa";
2 | import { styled } from "~/stitches.config";
3 | import { Row } from "./layout/Row";
4 |
5 | export function Callout({ label, children }) {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {label}
16 |
17 | {children}
18 |
19 | );
20 | }
21 |
22 | const Details = styled("details", {
23 | border: "1px solid $gray8",
24 | padding: "$4",
25 | borderRadius: "$base",
26 |
27 | "> :not(summary)": {
28 | marginTop: "$2",
29 | },
30 |
31 | "[data-opened-icon]": {
32 | display: "none",
33 | },
34 |
35 | "&[open]": {
36 | "[data-closed-icon]": {
37 | display: "none",
38 | },
39 |
40 | "[data-opened-icon]": {
41 | display: "flex",
42 | },
43 | },
44 | });
45 |
46 | const Summary = styled(Row, {
47 | fontWeight: "bold",
48 | color: "$gray11",
49 | gap: "$2",
50 | cursor: "pointer",
51 | });
52 |
--------------------------------------------------------------------------------
/components/Caption.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 |
3 | export const Caption = styled("figcaption", {
4 | marginTop: "$2",
5 | color: "$gray11",
6 | fontSize: "$sm",
7 | });
8 |
--------------------------------------------------------------------------------
/components/ChangeIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { darkTheme, styled } from "~/stitches.config";
4 |
5 | export const ChangeIndicator = ({ value, children }) => {
6 | const [state, setState] = React.useState("idle");
7 | const [mounted, setMounted] = React.useState(false);
8 |
9 | React.useEffect(() => {
10 | if (!mounted) {
11 | setMounted(true);
12 | } else {
13 | setState("active");
14 | const id = setTimeout(() => setState("idle"), 1000);
15 | return () => clearTimeout(id);
16 | }
17 | }, [value]);
18 |
19 | return (
20 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | const Wrapper = styled(motion.span, {
38 | "--base-color": "255, 255, 255",
39 |
40 | [`.${darkTheme} &`]: {
41 | "--base-color": "75, 75, 75",
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/components/Checkbox/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from "./Checkbox";
2 |
3 | export const Default = () => (
4 |
5 | );
6 |
--------------------------------------------------------------------------------
/components/Checkbox/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { darkTheme, styled } from "~/stitches.config";
3 | import { violet, blackA } from "@radix-ui/colors";
4 | import { FaCheck } from "react-icons/fa";
5 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
6 |
7 | const StyledCheckbox = styled(CheckboxPrimitive.Root, {
8 | all: "unset",
9 | backgroundColor: "white",
10 | width: 22,
11 | height: 22,
12 | borderRadius: 4,
13 | display: "flex",
14 | alignItems: "center",
15 | justifyContent: "center",
16 | border: "1px solid $gray8",
17 | "&:hover": { backgroundColor: violet.violet3 },
18 | "&:focus": { borderColor: "$gray12" },
19 |
20 | [`.${darkTheme} &`]: {
21 | backgroundColor: "$gray2",
22 | },
23 | });
24 |
25 | const StyledIndicator = styled(CheckboxPrimitive.Indicator, {
26 | display: "flex",
27 | alignItems: "center",
28 | justifyContent: "center",
29 | });
30 |
31 | // Your app...
32 | const Flex = styled("div", { display: "flex" });
33 | const Label = styled("label", {
34 | userSelect: "none",
35 | });
36 |
37 | export const Checkbox = ({ label, ...props }) => {
38 | const id = React.useId();
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {label}
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/components/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Checkbox'
--------------------------------------------------------------------------------
/components/Content.tsx:
--------------------------------------------------------------------------------
1 | import { darkTheme, styled } from "~/stitches.config";
2 |
3 | export const Content = styled("div", {
4 | "> *": {
5 | gridColumn: "1",
6 | },
7 |
8 | "> figure": {
9 | marginTop: "$4",
10 | marginBottom: "$8",
11 | },
12 |
13 | "> .note": {
14 | gridColumn: "1 / -1",
15 | },
16 |
17 | "> .full-width": {
18 | gridColumn: "1 / -1",
19 | marginTop: "$4",
20 | marginBottom: "$8",
21 | width: "100%",
22 | },
23 |
24 | "> :where(:not(:last-child))": {
25 | marginBottom: "$4",
26 | },
27 |
28 | h2: {
29 | fontFamily: "$serif",
30 | },
31 |
32 | "code:not([data-rehype-pretty-code-fragment] code, pre code)": {
33 | background: "$gray7",
34 | padding: 2,
35 | fontSize: "$sm",
36 |
37 | [`.${darkTheme} &`]: {
38 | background: "$gray4",
39 | },
40 | },
41 |
42 | "*": {
43 | "&[data-theme='dark']": {
44 | display: "none",
45 | },
46 |
47 | [`.${darkTheme} &`]: {
48 | "&[data-theme='light']": {
49 | display: "none",
50 | },
51 | "&[data-theme='dark']": {
52 | display: "revert",
53 | },
54 | },
55 | },
56 |
57 | "[data-rehype-pretty-code-fragment] pre, > pre": {
58 | marginTop: "$4",
59 | marginBottom: "$8",
60 | whiteSpace: "pre-wrap",
61 | border: "1px solid $gray8",
62 | padding: "$4",
63 | borderRadius: "$base",
64 | fontSize: "$sm",
65 | overflowX: "auto",
66 | background: "$gray4",
67 | position: "relative",
68 |
69 | [`.${darkTheme} &`]: {
70 | border: "1px solid $gray6",
71 | background: "$gray2",
72 | },
73 | },
74 |
75 | blockquote: {
76 | paddingLeft: "$4",
77 | borderLeft: "2px solid $gray8",
78 | color: "$gray11",
79 | fontStyle: "italic",
80 | },
81 |
82 | hr: {
83 | marginTop: "$6",
84 | marginBottom: "$12",
85 | width: "30%",
86 | borderTop: "1px solid $gray8",
87 | },
88 |
89 | "[data-rehype-pretty-code-fragment] > pre": {
90 | marginBottom: "$4",
91 | },
92 | });
93 |
--------------------------------------------------------------------------------
/components/Figure.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 |
3 | export const Figure = styled("figure");
4 |
5 | export const Caption = styled("p", {
6 | padding: "$0 $8",
7 | marginTop: "$4",
8 | fontSize: "$sm",
9 | textAlign: "center",
10 | });
11 |
--------------------------------------------------------------------------------
/components/FullWidth.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const FullWidth = ({ children }) => {
4 | return {children}
;
5 | };
6 |
--------------------------------------------------------------------------------
/components/Heading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { styled } from "~/stitches.config";
4 | import { getId } from "~/lib/utils";
5 |
6 | export const Heading = ({ children }) => {
7 | const id = getId(children);
8 | return {children} ;
9 | };
10 |
11 | const H2 = styled("h2", {
12 | fontSize: "$2xl",
13 | fontFamily: "$serif",
14 | fontWeight: 500,
15 | position: "relative",
16 | scrollMarginTop: "$16",
17 | marginTop: "$24",
18 | marginBottom: "$8",
19 |
20 | "&:before": {
21 | content: "",
22 | position: "absolute",
23 | left: 0,
24 | top: "-$4",
25 | width: "$6",
26 | height: 3,
27 | background: "currentColor",
28 | },
29 | });
30 |
31 | export const Subheading = ({ children }) => {
32 | const id = getId(children);
33 | return {children} ;
34 | };
35 |
36 | const H3 = styled("h3", {
37 | fontSize: "$xl",
38 | scrollMarginTop: "$28",
39 | marginTop: "$8",
40 | marginBottom: "$6",
41 | fontWeight: "normal",
42 | });
43 |
--------------------------------------------------------------------------------
/components/Image.tsx:
--------------------------------------------------------------------------------
1 | import { GridBackground } from "./Grid";
2 |
3 | type ImageProps = {
4 | href: string;
5 | };
6 |
7 | export function Image({ href }: ImageProps) {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/components/Img.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import { FullWidth } from "./FullWidth";
4 |
5 | type ImgProps = {
6 | fullWidth?: boolean;
7 | } & React.ComponentProps;
8 |
9 | export const Img = ({ fullWidth = false, ...props }: ImgProps) => {
10 | const Wrapper = fullWidth ? FullWidth : "figure";
11 | return (
12 |
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/components/Link/Link.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "./Link";
2 |
3 | export const Default = () => Hello;
4 |
--------------------------------------------------------------------------------
/components/Link/Link.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { styled } from "~/stitches.config";
3 |
4 | type LinkProps = {
5 | href?: string;
6 | children: React.ReactNode;
7 | };
8 |
9 | export const Link = ({ href, children }: LinkProps) => {
10 | if (href?.startsWith("http")) {
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | }
17 | return {children} ;
18 | };
19 |
20 | const LinkWrapper = styled("a", {
21 | color: "$blue9",
22 | textDecoration: "none",
23 |
24 | "&:hover": {
25 | textDecoration: "wavy underline $colors$blue8",
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/components/Link/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Link'
--------------------------------------------------------------------------------
/components/MobileBottomBar.tsx:
--------------------------------------------------------------------------------
1 | import type { Heading } from "~/lib/content.server";
2 | import { styled } from "~/stitches.config";
3 | import { MobileNavIsland } from "./MobileNavIsland";
4 |
5 | export const MobileBottomBar = ({ headings }: { headings: Heading[] }) => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | const Wrapper = styled("div", {
14 | position: "fixed",
15 | width: "min(800px, 100vw)",
16 | bottom: 0,
17 | padding: "$4",
18 | left: "50%",
19 | transform: "translateX(-50%)",
20 | zIndex: 20,
21 |
22 | "@media (min-width: 72rem)": {
23 | display: "none",
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/components/MobileNavIsland/MobileNavIsland.stories.tsx:
--------------------------------------------------------------------------------
1 | import { MobileNavIsland } from "./MobileNavIsland";
2 |
3 | const headings = [
4 | {
5 | text: "Layout Changes",
6 | id: "layout-changes",
7 | },
8 | {
9 | text: "Animating With CSS",
10 | id: "animating-with-css",
11 | },
12 | {
13 | text: "Introducing FLIP",
14 | id: "introducing-flip",
15 | },
16 | {
17 | text: "Animating Size",
18 | id: "animating-size",
19 | },
20 | {
21 | text: "Consolidating Size with Position",
22 | id: "consolidating-size-with-position",
23 | },
24 | {
25 | text: "Correcting Child Distortions",
26 | id: "correcting-child-distortions",
27 | },
28 | {
29 | text: "Summary",
30 | id: "summary",
31 | },
32 | ];
33 |
34 | export const Default = () => ;
35 |
--------------------------------------------------------------------------------
/components/MobileNavIsland/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './MobileNavIsland'
--------------------------------------------------------------------------------
/components/NewsletterForm.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 | import { SubscribeInput } from "./SubscribeInput";
3 |
4 | export function NewsletterForm() {
5 | return (
6 |
7 | Newsletter
8 |
9 | Sign up for my newsletter to get new content sent straight to your
10 | inbox! You'll also receive updates on whatever I'm working on and
11 | anything I find interesting.
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const Wrapper = styled("div", {
19 | border: "1px solid $gray8",
20 | padding: "$4",
21 | borderRadius: "$base",
22 |
23 | "> :not(:last-child)": {
24 | marginBottom: "$2",
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/components/Note.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 |
3 | export const Root = ({ children }) => {
4 | return {children} ;
5 | };
6 |
7 | const RootWrapper = styled("div", {
8 | display: "grid",
9 | gridTemplateColumns: "min(100%, 65ch) 1fr",
10 | position: "relative",
11 |
12 | "> *": {
13 | gridColumn: 1,
14 | },
15 |
16 | "> :not(:first-child, aside)": {
17 | marginTop: "$4",
18 | },
19 |
20 | "> aside": {
21 | marginTop: "$4",
22 | },
23 |
24 | "@md": {
25 | "> aside": {
26 | marginTop: 0,
27 | gridColumn: 2,
28 | gridRow: "1 / -1",
29 | },
30 | },
31 | });
32 |
33 | export const Content = ({ children }) => {
34 | return ;
35 | };
36 |
37 | const Aside = styled("aside", {
38 | color: "$gray11",
39 |
40 | "@md": {
41 | position: "absolute",
42 | fontSize: "$sm",
43 | paddingLeft: "$4",
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/components/OrderedList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { styled } from "~/stitches.config";
4 |
5 | export function OrderedList({ children }: { children: React.ReactNode }) {
6 | return (
7 |
8 | {React.Children.toArray(children)
9 | .filter(Boolean)
10 | .map((child: any, index) =>
11 | child.props ? (
12 |
13 | {child.props.children}
14 |
15 | ) : null
16 | )}
17 |
18 | );
19 | }
20 |
21 | const ListWrapper = styled("ol", {
22 | listStyle: "none",
23 | counterReset: "counts 0",
24 |
25 | "> :not([hidden]) ~ :not([hidden]), ol": {
26 | marginTop: "$2",
27 | },
28 | });
29 |
30 | const ListItem = styled("li", {
31 | counterIncrement: "counts 1",
32 | display: "flex",
33 |
34 | "&:before": {
35 | content: 'counter(counts) ". "', // {count}.
36 | paddingRight: "$4",
37 | fontFamily: "$mono",
38 | fontWeight: "500",
39 | color: "$gray11",
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/components/PlayButton.tsx:
--------------------------------------------------------------------------------
1 | import { FaPlay, FaPause } from "react-icons/fa";
2 | import { styled, darkTheme } from "~/stitches.config";
3 | import { Row } from "./layout/Row";
4 |
5 | type PlayButtonProps = {
6 | isPlaying: boolean;
7 | onClick: () => void;
8 | secondary?: boolean;
9 | };
10 |
11 | export const PlayButton = ({
12 | onClick,
13 | isPlaying,
14 | secondary,
15 | }: PlayButtonProps) => {
16 | return (
17 |
24 | {isPlaying ? : }
25 |
26 | );
27 | };
28 |
29 | const Wrapper = styled(Row, {
30 | background: "$blue6",
31 | border: "1px solid black",
32 | width: "$8",
33 | height: "$8",
34 | borderRadius: 4,
35 | boxShadow: "$md",
36 | flexShrink: 0,
37 | fontSize: "$sm",
38 | cursor: "pointer",
39 |
40 | "&:hover": {
41 | color: "$gray1",
42 | background: "$blue9",
43 | borderColor: "$blue11",
44 | },
45 |
46 | [`.${darkTheme} &`]: {
47 | background: "$blue9",
48 | borderColor: "$blue9",
49 | color: "$gray1",
50 |
51 | "&:hover": {
52 | color: "inherit",
53 | background: "$blue10",
54 | borderColor: "$blue10",
55 | },
56 | },
57 |
58 | variants: {
59 | secondary: {
60 | true: {
61 | color: "$gray11",
62 | background: "none",
63 | boxShadow: "none",
64 | border: "none",
65 |
66 | "&:hover": {
67 | color: "$gray11",
68 | background: "$gray7",
69 | border: "none",
70 | },
71 |
72 | [`.${darkTheme} &`]: {
73 | background: "none",
74 | border: "none",
75 |
76 | "&:hover": {
77 | color: "$gray11",
78 | background: "$gray7",
79 | border: "none",
80 | },
81 | },
82 | },
83 | },
84 | },
85 | });
86 |
--------------------------------------------------------------------------------
/components/PrimaryButton/PrimaryButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { PrimaryButton } from './PrimaryButton'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/PrimaryButton/PrimaryButton.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import type { ComponentPropsWithoutRef } from "react";
3 | import { darkTheme, styled } from "~/stitches.config";
4 |
5 | export const PrimaryButton = (
6 | props: ComponentPropsWithoutRef
7 | ) => {
8 | return (
9 |
10 |
11 | <_Button whileTap={{ x: 2, y: 2 }} {...props} />
12 |
13 | );
14 | };
15 |
16 | const Wrapper = styled("div", {
17 | position: "relative",
18 | width: "fit-content",
19 |
20 | "&:hover": {
21 | span: {
22 | transform: "translate(2px, 2px)",
23 | },
24 | button: {
25 | background:
26 | "linear-gradient(-45deg, var(--gradient-end, $colors$blue6), var(--gradient-start, $colors$blue4))",
27 | color: "$gray12",
28 |
29 | [`.${darkTheme} &`]: {
30 | background:
31 | "linear-gradient(-45deg, var(--gradient-end, $colors$blue9), var(--gradient-start, $colors$blue6))",
32 | color: "$gray1",
33 | borderColor: "$gray1",
34 | },
35 | },
36 | },
37 | });
38 |
39 | const Shadow = styled("span", {
40 | position: "absolute",
41 | inset: 0,
42 | background: "$gray12",
43 | borderRadius: "$base",
44 | transition: "all 0.2s ease-out",
45 | zIndex: -1,
46 | });
47 |
48 | const _Button = styled(motion.button, {
49 | background: "$gray12",
50 | padding: "$2 $4",
51 | paddingTop: "calc($space$2 + 2px)",
52 | borderRadius: "$base",
53 | border: "1px solid $gray12",
54 | color: "$gray1",
55 | fontWeight: "bold",
56 | cursor: "pointer",
57 | fontFamily: "$serif",
58 | transition: "all 0.2s ease-out",
59 | position: "relative",
60 |
61 | [`.${darkTheme} &`]: {
62 | background: "$gray1",
63 | color: "$gray12",
64 | },
65 |
66 | variants: {
67 | small: {
68 | true: {
69 | fontSize: "$sm",
70 | padding: "$1 $2",
71 | paddingTop: "calc($space$1 + 2px)",
72 | },
73 | },
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/components/PrimaryButton/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './PrimaryButton'
--------------------------------------------------------------------------------
/components/ProblemStatement.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 |
3 | export function ProblemStatement({ children }) {
4 | return (
5 |
6 | The Problem
7 | {children}
8 |
9 | );
10 | }
11 |
12 | const Wrapper = styled("aside", {
13 | padding: "$4",
14 | border: "1px solid $gray8",
15 | borderRadius: "$base",
16 | });
17 |
--------------------------------------------------------------------------------
/components/Quiz/Quiz.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Quiz } from "./Quiz";
2 |
3 | export const Default = () => (
4 |
5 | What is box referring to?
6 |
7 | The initial position
8 | The final position
9 |
10 |
11 | Not quite! Because effects run after the component renders, the box is
12 | actually referring to the square's final position.
13 |
14 |
15 | That's right! Because effects run after the component renders, the box is
16 | referring to the square's final position.
17 |
18 |
19 | );
20 |
21 | export const WithSpoiler = () => (
22 |
23 |
24 | What is 2 + 2?
25 |
26 | 22
27 | 4
28 |
29 |
30 |
31 | The right answer is 4 .
32 |
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/components/Quiz/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Quiz'
--------------------------------------------------------------------------------
/components/Sandbox/Sandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FullWidth } from "../FullWidth";
2 | import { Sandbox } from "./Sandbox";
3 |
4 | const code = `export default function App() {
5 | const count = 1
6 | return Hello World
7 | }`;
8 |
9 | export const Default = () => ;
10 |
11 | export const Wide = () => (
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/Sandbox/codemirror.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 | import { syntaxTree } from "@codemirror/language";
3 |
4 | export const clickHandlerExtension = EditorView.domEventHandlers({
5 | click: (evt, view) => {
6 | const [range] = view.state.selection.ranges;
7 | const ast = syntaxTree(view.state);
8 | const node = ast.resolveInner(range.from, -1);
9 | console.log(node.type, node.from, view.domAtPos(node.from));
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/components/Sandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Sandbox'
--------------------------------------------------------------------------------
/components/SecondaryButton.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 |
3 | export const SecondaryButton = styled("button", {
4 | width: "$8",
5 | height: "$8",
6 | borderRadius: 4,
7 | flexShrink: 0,
8 | display: "flex",
9 | alignItems: "center",
10 | justifyContent: "center",
11 | cursor: "pointer",
12 | color: "$gray11",
13 | background: "none",
14 | boxShadow: "none",
15 | border: "none",
16 |
17 | "&:hover": {
18 | color: "$gray11",
19 | background: "$gray7",
20 | border: "none",
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/components/SharedState.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Context = React.createContext(null);
4 |
5 | export const SharedState = ({ initialState, children }) => {
6 | const [state, setState] = React.useState(initialState);
7 | return (
8 | {children}
9 | );
10 | };
11 |
12 | export const useSharedState = (defaultValue) => {
13 | const localState = React.useState(defaultValue);
14 | const state = React.useContext(Context);
15 |
16 | if (!state) {
17 | return localState;
18 | }
19 |
20 | return [state.state, state.setState];
21 | };
22 |
--------------------------------------------------------------------------------
/components/Slider.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithoutRef } from "react";
2 | import * as SliderPrimitive from "@radix-ui/react-slider";
3 | import { darkTheme, styled } from "~/stitches.config";
4 |
5 | export const Slider = (props: ComponentPropsWithoutRef) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | const SliderRoot = styled(SliderPrimitive.Root, {
17 | position: "relative",
18 | width: "100%",
19 | display: "flex",
20 | alignItems: "center",
21 | });
22 |
23 | const Track = styled(SliderPrimitive.Track, {
24 | position: "relative",
25 | background: "$gray7",
26 | flexGrow: 1,
27 | height: 4,
28 | borderRadius: 4,
29 |
30 | [`.${darkTheme} &`]: {
31 | background: "$gray4",
32 | },
33 | });
34 |
35 | const Range = styled(SliderPrimitive.Range, {
36 | position: "absolute",
37 | backgroundColor: "$gray8",
38 | height: "100%",
39 | });
40 |
41 | const Thumb = styled(SliderPrimitive.Thumb, {
42 | display: "block",
43 | background: "$gray3",
44 | border: "1px solid $gray8",
45 | width: "$4",
46 | height: "$6",
47 | borderRadius: 4,
48 | boxShadow: "$sm",
49 | transition: "transform 0.1s ease-out",
50 | transform: "scale(1)",
51 | cursor: "pointer",
52 |
53 | "&:hover": {
54 | color: "$gray1",
55 | background: "$blue9",
56 | borderColor: "$blue11",
57 | transform: "scale(0.9)",
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/components/Spoiler.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { styled } from "~/stitches.config";
3 | import { Content } from "./Content";
4 | import { ToggleButton } from "./Visualizer";
5 |
6 | export const Spoiler = ({ children }) => {
7 | const [revealed, setRevealed] = React.useState(false);
8 | if (revealed) return <>{children}>;
9 | return (
10 |
11 | {children}
12 |
24 | setRevealed(true)}>
25 | Reveal Answer
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | const Box = styled("div", {});
33 |
--------------------------------------------------------------------------------
/components/SubscribeButton/SubscribeButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SubscribeButton } from './SubscribeButton'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/SubscribeButton/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SubscribeButton'
--------------------------------------------------------------------------------
/components/SvgGridWrapper/SvgGridWrapper.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SvgGridWrapper } from "./SvgGridWrapper";
2 |
3 | export const Default = () => (
4 |
5 | Hello!
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/components/SvgGridWrapper/SvgGridWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { GridBackground } from "../Grid";
3 |
4 | export const DEFAULT_HEIGHT = 300;
5 |
6 | type SvgGridWrapperProps = {
7 | height?: number;
8 | children: React.ReactNode;
9 | } & React.ComponentPropsWithoutRef;
10 |
11 | export const SvgGridWrapper = React.forwardRef<
12 | HTMLDivElement,
13 | SvgGridWrapperProps
14 | >(function SvgGridWrapper(
15 | { height = DEFAULT_HEIGHT, children, ...props },
16 | ref
17 | ) {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | });
26 |
--------------------------------------------------------------------------------
/components/SvgGridWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SvgGridWrapper'
--------------------------------------------------------------------------------
/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { BiSun } from "react-icons/bi";
3 | import { styled } from "~/stitches.config";
4 |
5 | import { SecondaryButton } from "./SecondaryButton";
6 |
7 | export const ThemeToggle = () => {
8 | const { theme, setTheme } = useTheme();
9 | return (
10 | (theme === "dark" ? setTheme("light") : setTheme("dark"))}
12 | >
13 |
14 |
15 | );
16 | };
17 |
18 | const Button = styled(SecondaryButton, {
19 | fontSize: "$xl",
20 | width: "$10",
21 | height: "$10",
22 | });
23 |
--------------------------------------------------------------------------------
/components/Visualizer/Visualizer.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Visualizer } from './Visualizer'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/Visualizer/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Visualizer'
--------------------------------------------------------------------------------
/components/home/Debugger/Debugger.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Debugger } from './Debugger'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/home/Debugger/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Debugger'
--------------------------------------------------------------------------------
/components/home/FramerMagicMotion/FramerMagicMotion.stories.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "~/stitches.config";
2 | import { FramerMagicMotion } from "./FramerMagicMotion";
3 |
4 | export const Default = () => (
5 |
6 |
7 |
8 | );
9 |
10 | const Wrapper = styled("div", {
11 | width: 400,
12 | aspectRatio: 1,
13 | });
14 |
--------------------------------------------------------------------------------
/components/home/FramerMagicMotion/FramerMagicMotion.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { VisualWrapper } from "../shared";
3 | import { darkTheme, styled } from "~/stitches.config";
4 | import {
5 | SvgBackgroundGradient,
6 | getFillFromId,
7 | } from "~/components/utils/SvgBackgroundGradient";
8 |
9 | export const FramerMagicMotion = () => {
10 | const id = React.useId();
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | const Point = styled("circle", {
34 | stroke: "$gray11",
35 | fill: "$gray3",
36 | strokeWidth: 0.2,
37 | });
38 |
39 | const Path = styled("path", {
40 | fill: "none",
41 | stroke: "$gray12",
42 | strokeDasharray: "2 1",
43 | strokeWidth: 0.2,
44 | });
45 |
46 | const Rect = styled("rect", {
47 | width: 50,
48 | height: 50,
49 | strokeWidth: 0.2,
50 | stroke: "$gray12",
51 |
52 | [`.${darkTheme} &`]: {
53 | stroke: "$gray1",
54 | },
55 |
56 | variants: {
57 | type: {
58 | secondary: {
59 | fill: "$gray5",
60 | stroke: "$gray10",
61 | strokeDasharray: "2 1",
62 |
63 | [`.${darkTheme} &`]: {
64 | stroke: "$gray10",
65 | fill: "$gray3",
66 | },
67 | },
68 | shadow: {
69 | fill: "$gray12",
70 |
71 | [`.${darkTheme} &`]: {
72 | fill: "$gray1",
73 | },
74 | },
75 | },
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/components/home/FramerMagicMotion/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FramerMagicMotion'
--------------------------------------------------------------------------------
/components/home/FramerMotionKeys/FramerMotionKeys.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FramerMotionKeys } from './FramerMotionKeys'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/home/FramerMotionKeys/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FramerMotionKeys'
--------------------------------------------------------------------------------
/components/home/HowArraysWork/HowArraysWork.stories.tsx:
--------------------------------------------------------------------------------
1 | import { HowArraysWork } from './HowArraysWork'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/home/HowArraysWork/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './HowArraysWork'
--------------------------------------------------------------------------------
/components/home/SlidingWindow/SlidingWindow.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SlidingWindow } from './SlidingWindow'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/home/SlidingWindow/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SlidingWindow'
--------------------------------------------------------------------------------
/components/home/SvgPaths/SvgPaths.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SvgPaths } from './SvgPaths'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/home/SvgPaths/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SvgPaths'
--------------------------------------------------------------------------------
/components/home/TokenizerVisual/TokenizerVisual.stories.tsx:
--------------------------------------------------------------------------------
1 | import { TokenizerVisual } from './TokenizerVisual'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/components/home/TokenizerVisual/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './TokenizerVisual'
--------------------------------------------------------------------------------
/components/home/shared.tsx:
--------------------------------------------------------------------------------
1 | import { Content } from "../Visualizer";
2 | import { styled } from "~/stitches.config";
3 |
4 | export const VisualWrapper = ({ children }) => {
5 | return (
6 |
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | const Background = styled(Content, {
14 | position: "absolute",
15 | inset: "$6",
16 | borderRadius: 9999,
17 | border: "1px solid $gray8",
18 | });
19 |
20 | const Wrapper = styled("div", {
21 | height: "100%",
22 | position: "relative",
23 | });
24 |
--------------------------------------------------------------------------------
/components/layout/Row.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { styled } from "~/stitches.config";
3 |
4 | export const Row = styled(motion.div, {
5 | display: "flex",
6 |
7 | variants: {
8 | center: {
9 | all: {
10 | alignItems: "center",
11 | justifyContent: "center",
12 | },
13 | vertical: {
14 | alignItems: "center",
15 | },
16 | horizontal: {
17 | justifyContent: "center",
18 | },
19 | },
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/components/utils/SvgBackgroundGradient.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { darkTheme, styled } from "~/stitches.config";
3 |
4 | export type Color = "yellow" | "green";
5 |
6 | export type SvgBackgroundGradientProps = {
7 | id: string;
8 | color?: Color;
9 | };
10 |
11 | export const getFillFromId = (id: string) => `url('#${id}')`;
12 |
13 | export function SvgBackgroundGradient({
14 | id,
15 | color,
16 | }: SvgBackgroundGradientProps) {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | const StopStart = styled("stop", {
28 | stopColor: "$colors$blue4",
29 |
30 | [`.${darkTheme} &`]: {
31 | stopColor: "$colors$blue7",
32 | },
33 |
34 | variants: {
35 | color: {
36 | green: {
37 | stopColor: "$colors$green4",
38 | [`.${darkTheme} &`]: {
39 | stopColor: "$colors$green7",
40 | },
41 | },
42 | yellow: {
43 | stopColor: "$colors$yellow4",
44 | [`.${darkTheme} &`]: {
45 | stopColor: "$colors$yellow7",
46 | },
47 | },
48 | red: {
49 | stopColor: "$colors$red4",
50 | [`.${darkTheme} &`]: {
51 | stopColor: "$colors$red7",
52 | },
53 | },
54 | },
55 | },
56 | });
57 |
58 | const StopEnd = styled("stop", {
59 | stopColor: "$colors$blue6",
60 |
61 | [`.${darkTheme} &`]: {
62 | stopColor: "$colors$blue9",
63 | },
64 |
65 | variants: {
66 | color: {
67 | green: {
68 | stopColor: "$colors$green6",
69 | [`.${darkTheme} &`]: {
70 | stopColor: "$colors$green9",
71 | },
72 | },
73 | yellow: {
74 | stopColor: "$colors$yellow6",
75 | [`.${darkTheme} &`]: {
76 | stopColor: "$colors$yellow9",
77 | },
78 | },
79 | red: {
80 | stopColor: "$colors$red6",
81 | [`.${darkTheme} &`]: {
82 | stopColor: "$colors$red9",
83 | },
84 | },
85 | },
86 | },
87 | });
88 |
--------------------------------------------------------------------------------
/content/aoc/2022/day-01/index.tsx:
--------------------------------------------------------------------------------
1 | export function getMaxTotalCalories(text: string) {
2 | const groups = text.split("\n\n");
3 |
4 | const totalCaloriesByElf = [];
5 | for (const group of groups) {
6 | const calories = group
7 | .split("\n")
8 | .reduce((acc, countString) => acc + Number(countString), 0);
9 | totalCaloriesByElf.push(calories);
10 | }
11 |
12 | totalCaloriesByElf.sort((a, b) => b - a);
13 | return totalCaloriesByElf[0] + totalCaloriesByElf[1] + totalCaloriesByElf[2];
14 | }
15 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/AnimationShowcase/AnimationShowcase.stories.tsx:
--------------------------------------------------------------------------------
1 | import { AnimationShowcase } from './AnimationShowcase'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/AnimationShowcase/AnimationShowcase.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useInterval from "@use-it/interval";
3 | import { FullWidth } from "~/components/FullWidth";
4 | import { Content, Visualizer } from "~/components/Visualizer";
5 | import { styled } from "~/stitches.config";
6 | import { KanjiViewer } from "../KanjiCarousel";
7 | import { Button } from "../NextButton";
8 |
9 | export const AnimationShowcase = ({ speed = 1500 }) => {
10 | const [toggled, toggle] = React.useReducer((state) => !state, false);
11 | const [currentIndex, next] = React.useReducer((index) => {
12 | return index === 2 ? 0 : index + 1;
13 | }, 0);
14 |
15 | useInterval(() => {
16 | next();
17 | toggle();
18 | }, speed);
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | const Wrapper = styled("figure", {
40 | marginLeft: "-$4",
41 | marginRight: "-$4",
42 | padding: "0 $4",
43 | overflowX: "auto",
44 | width: "calc(100% + 2 * $space$4) !important",
45 | display: "flex",
46 |
47 | "@md": {
48 | marginLeft: 0,
49 | marginRight: 0,
50 | padding: 0,
51 | width: "100% !important",
52 | },
53 | });
54 |
55 | const VisualWrapper = styled(Visualizer, {
56 | minWidth: 300,
57 | flex: 1,
58 | });
59 |
60 | const ContentWrapper = styled(Content, {
61 | display: "flex",
62 | padding: "$10 0",
63 | justifyContent: "center",
64 | alignItems: "center",
65 | height: "100%",
66 | });
67 |
68 | const Box = styled("div", {});
69 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/AnimationShowcase/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './AnimationShowcase'
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/CodeQuiz/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './CodeQuiz'
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/Counter/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Counter";
2 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/KanjiCarousel/KanjiCarousel.stories.tsx:
--------------------------------------------------------------------------------
1 | import { KanjiCarousel } from "./KanjiCarousel";
2 | import { KanjiCarouselSlide } from "./KanjiCarouselSlide";
3 |
4 | export const Default = () => ;
5 |
6 | export const Slide = () => ;
7 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/KanjiCarousel/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./KanjiCarousel";
2 | export * from "./KanjiCarouselSlide";
3 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/NextButton/NextButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { NextButton } from './NextButton'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/NextButton/NextButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import { FullWidth } from "~/components/FullWidth";
4 | import { Content, Visualizer } from "~/components/Visualizer";
5 | import { styled } from "~/stitches.config";
6 |
7 | export const Button = ({ toggled, onClick = () => {} }) => {
8 | return (
9 |
30 |
36 | {toggled ? "Next" : "Show Answer"}
37 |
38 |
39 | );
40 | };
41 |
42 | export const NextButton = () => {
43 | const [toggled, toggle] = React.useReducer((state) => !state, false);
44 | return (
45 |
46 |
47 |
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | const Box = styled("div", {});
63 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/NextButton/Sandbox.tsx:
--------------------------------------------------------------------------------
1 | import { Sandbox } from "~/components/Sandbox";
2 |
3 | const css = `
4 | html {
5 | background: hsl(0, 0%, 13.6%);
6 | color: white;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | padding: 16px;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13 | }
14 |
15 | #motion {
16 | border: 1px solid hsl(0, 0%, 31.2%);
17 | background: hsl(0, 0%, 8.5%);
18 | padding: 8px 16px;
19 | border-radius: 8px;
20 | color: white;
21 | cursor: pointer;
22 | outline: none;
23 | }
24 |
25 | #main {
26 | margin-top: 32px;
27 | display: flex;
28 | justify-content: center;
29 | }
30 | `;
31 |
32 | const motionCode = `import React from 'react'
33 | import { motion } from 'framer-motion'
34 |
35 | export function NextButton() {
36 | const [toggled, toggle] = React.useReducer((state) => !state, false);
37 |
38 | return (
39 |
40 |
45 | {toggled ? "Next" : "Show Answer"}
46 |
47 |
48 | )
49 | }`;
50 |
51 | const code = `import React from 'react'
52 | import { NextButton } from './button'
53 | import './styles.css'
54 |
55 | export default function App() {
56 | const [toggled, toggle] = React.useReducer(state => !state, false)
57 |
58 | return (
59 |
60 |
61 |
62 | )
63 | }
64 | `;
65 |
66 | export const NextButtonSandbox = () => {
67 | return (
68 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/NextButton/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./NextButton";
2 | export * from "./Sandbox";
3 |
--------------------------------------------------------------------------------
/content/keys-in-framer-motion/components/RefreshComponent.tsx:
--------------------------------------------------------------------------------
1 | import { Sandbox } from "~/components/Sandbox";
2 |
3 | const css = `
4 | html {
5 | background: hsl(0, 0%, 13.6%);
6 | color: white;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | padding: 16px;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13 | }
14 |
15 | #motion {
16 | border: 1px solid hsl(208, 100%, 47.3%);
17 | background: hsl(206, 100%, 30%);
18 | width: 120px;
19 | aspect-ratio: 1;
20 | border-radius: 8px;
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | }
25 |
26 | #wrapper {
27 | margin-top: 8px;
28 | display: flex;
29 | }
30 | `;
31 |
32 | const motionCode = `import React from 'react'
33 | import { motion } from 'framer-motion'
34 |
35 | export function Motion() {
36 | React.useEffect(() => {
37 | console.log(' mounted')
38 | return () => console.log(' unmounted')
39 | }, [])
40 |
41 | return
42 | }`;
43 |
44 | const code = `import React from 'react'
45 | import { Motion } from './motion'
46 | import { Refresh } from './refresh'
47 | import './styles.css'
48 |
49 | export default function App() {
50 | const [toggled, toggle] = React.useReducer(state => !state, false)
51 |
52 | return (
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
60 | `;
61 |
62 | const refresh = `import React from 'react'
63 |
64 | export const Refresh = ({ children }) => {
65 | const [key, setKey] = React.useState(0);
66 | return (
67 | <>
68 | setKey(key + 1)}>Refresh
69 | {children}
70 | >
71 | );
72 | }
73 | `;
74 |
75 | export const RefreshComponent = () => {
76 | return (
77 |
94 | );
95 | };
96 |
--------------------------------------------------------------------------------
/content/magic-motion/components/CorrectedInverseAnimation/CorrectedInverseAnimation.stories.tsx:
--------------------------------------------------------------------------------
1 | import { CorrectedInverseAnimation } from "./CorrectedInverseAnimation";
2 |
3 | const from = (width, container) => {
4 | return {
5 | x: container.width - width / 2 - container.padding,
6 | y: container.height / 2,
7 | };
8 | };
9 |
10 | const to = (width, container) => {
11 | return {
12 | x: container.padding + width / 2,
13 | y: container.height / 2,
14 | };
15 | };
16 |
17 | export const Center = () => ;
18 |
19 | export const TopLeft = () => (
20 | ({
22 | x: container.width - width - container.padding,
23 | y: container.height / 2 - width / 2,
24 | })}
25 | to={(width, container) => ({
26 | x: container.padding,
27 | y: container.height / 2 - width / 2,
28 | })}
29 | origin="topLeft"
30 | />
31 | );
32 |
33 | export const Default = () => (
34 | ({
36 | x: container.width - width - container.padding,
37 | y: container.height / 2 - width / 2,
38 | })}
39 | to={(width, container) => ({
40 | x: container.padding,
41 | y: container.height / 2 - width / 2,
42 | })}
43 | />
44 | );
45 |
--------------------------------------------------------------------------------
/content/magic-motion/components/CorrectedInverseAnimation/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './CorrectedInverseAnimation'
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipExample/FlipExample.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipExample } from "./FlipExample";
2 |
3 | export const Default = () => ;
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipExample/FlipExample.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Visualizer, Content, ToggleButton } from "~/components/Visualizer";
4 | import { FullWidth } from "~/components/FullWidth";
5 | import { ChangeIndicator } from "~/components/ChangeIndicator";
6 | import { styled } from "~/stitches.config";
7 |
8 | import { Square, Controls, AlignmentText } from "../shared";
9 |
10 | export const FlipExample = () => {
11 | const [toggled, toggle] = React.useReducer((state) => !state, false);
12 |
13 | return (
14 |
15 |
16 |
17 | Toggle Layout
18 |
19 |
20 | justify-content:{" "}
21 |
22 | {toggled ? "flex-end" : "flex-start"}
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const DisplayOnlySquare = styled(Square, {
42 | pointerEvents: "none",
43 | });
44 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipExample/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FlipExample'
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipFirst/FlipFirst.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipFirst } from "./FlipFirst";
2 |
3 | export const Default = () => ;
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipFirst/FlipFirst.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useMachine } from "@xstate/react";
3 | import { assign } from "xstate";
4 |
5 | import { GridBackground } from "~/components/Grid";
6 | import { FullWidth } from "~/components/FullWidth";
7 |
8 | import { Tooltip, ContentWrapper, Square, XLine, YLine } from "../shared";
9 | import { machine } from "./machine";
10 |
11 | export const FlipFirst = () => {
12 | const buttonRef = React.useRef();
13 | const [state, send] = useMachine(machine, {
14 | actions: {
15 | measureBox: assign({
16 | box: () => {
17 | return buttonRef.current?.getBoundingClientRect();
18 | },
19 | }),
20 | },
21 | });
22 |
23 | const active = state.matches("measured");
24 | const showRulers = ["hovering", "measured"].some(state.matches);
25 |
26 | return (
27 |
28 |
29 |
30 | {showRulers && (
31 | <>
32 |
33 |
34 | >
35 | )}
36 | send("click")}
39 | onHoverStart={() => send("hover")}
40 | onHoverEnd={() => send("hoverEnd")}
41 | whileTap={{ scale: 0.95 }}
42 | active={active}
43 | >
44 | Click me!
45 |
46 | {active && (
47 |
53 | x: {state.context.box?.x.toFixed(1)}
54 | y: {state.context.box?.y.toFixed(1)}
55 |
56 | )}
57 |
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipFirst/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./FlipFirst";
2 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipFirst/machine.ts:
--------------------------------------------------------------------------------
1 | import { createMachine } from "xstate";
2 |
3 | export const machine = createMachine({
4 | id: "flipLast",
5 | initial: "idle",
6 | context: {
7 | box: null,
8 | },
9 | states: {
10 | idle: {
11 | on: {
12 | click: "measured",
13 | hover: "hovering",
14 | },
15 | },
16 | hovering: {
17 | on: {
18 | hoverEnd: "idle",
19 | click: "measured",
20 | },
21 | },
22 | measured: {
23 | type: "final",
24 | entry: ["measureBox"],
25 | },
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipInverse/FlipInverse.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipInverse } from './FlipInverse'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipInverse/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FlipInverse'
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipLast/FlipLast.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipLast } from "./FlipLast";
2 |
3 | export const Default = () => ;
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipLast/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./FlipLast";
2 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipLast/machine.ts:
--------------------------------------------------------------------------------
1 | import { createMachine } from "xstate";
2 |
3 | export const machine = createMachine({
4 | id: "flipLast",
5 | initial: "idle",
6 | context: {
7 | origin: null,
8 | box: null,
9 | },
10 | states: {
11 | idle: {
12 | on: {
13 | click: "toggled",
14 | toggle: "toggled",
15 | },
16 | },
17 | toggled: {
18 | entry: ["measureOrigin"],
19 | on: {
20 | hover: "hovering",
21 | click: "measured",
22 | },
23 | },
24 | hovering: {
25 | on: {
26 | hoverEnd: "toggled",
27 | click: "measured",
28 | },
29 | },
30 | measured: {
31 | type: "final",
32 | entry: ["measureBox"],
33 | },
34 | },
35 | });
36 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipLastReact/FlipLastReact.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipLastReact } from './FlipLastReact'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipLastReact/FlipLastReact.tsx:
--------------------------------------------------------------------------------
1 | import { FullWidth } from "~/components/FullWidth";
2 | import { MotionSandbox } from "../MotionSandbox";
3 |
4 | const motionCode = `import React from 'react'
5 |
6 | export default function Motion() {
7 | const squareRef = React.useRef()
8 |
9 | React.useLayoutEffect(() => {
10 | const box = squareRef.current?.getBoundingClientRect()
11 | if (box) { console.log(box.x, box.y) }
12 | })
13 |
14 | return
15 | }
16 | `;
17 |
18 | export const FlipLastReact = () => {
19 | return (
20 |
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipLastReact/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FlipLastReact'
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipOverview/FlipOverview.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipOverview } from './FlipOverview'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipOverview/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FlipOverview'
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipOverview/machine.ts:
--------------------------------------------------------------------------------
1 | import { createMachine } from "xstate";
2 |
3 | export const STATE_ORDER = ["idle", "first", "last", "inverse", "play"];
4 |
5 | export const machine = createMachine({
6 | id: "flipLast",
7 | initial: "idle",
8 | context: {
9 | firstBox: null,
10 | lastBox: null,
11 | },
12 | states: {
13 | idle: {
14 | on: {
15 | next: "first",
16 | },
17 | },
18 | first: {
19 | entry: ["measureFirst"],
20 | on: {
21 | next: "last",
22 | },
23 | },
24 | last: {
25 | entry: ["measureLast", "removeTransform"],
26 | on: {
27 | next: "inverse",
28 | prev: "first",
29 | },
30 | },
31 | inverse: {
32 | entry: ["invert"],
33 | on: {
34 | next: "play",
35 | prev: "last",
36 | },
37 | },
38 | play: {
39 | entry: ["play"],
40 | on: {
41 | prev: "inverse",
42 | next: "first",
43 | },
44 | },
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipPlay/FlipPlay.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FlipPlay } from './FlipPlay'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/FlipPlay/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FlipPlay'
--------------------------------------------------------------------------------
/content/magic-motion/components/InitialPositionSandbox/InitialPositionSandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { InitialPositionSandbox } from './InitialPositionSandbox'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InitialPositionSandbox/InitialPositionSandbox.tsx:
--------------------------------------------------------------------------------
1 | import { MotionSandbox } from "../MotionSandbox";
2 |
3 | const motionCode = `import React from 'react'
4 |
5 | export default function Motion() {
6 | const squareRef = React.useRef();
7 | const initialPositionRef = React.useRef();
8 |
9 | React.useLayoutEffect(() => {
10 | const box = squareRef.current?.getBoundingClientRect();
11 | if (box) {
12 | // final position
13 | console.log(box.x, box.y)
14 |
15 | // initial position
16 | console.log(
17 | initialPositionRef.current?.x,
18 | initialPositionRef.current?.y
19 | );
20 |
21 | initialPositionRef.current = box;
22 | }
23 | });
24 |
25 | return
;
26 | }
27 | `;
28 |
29 | export const InitialPositionSandbox = () => {
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InitialPositionSandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InitialPositionSandbox'
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseSandbox/InverseSandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { InverseSandbox } from './InverseSandbox'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseSandbox/InverseSandbox.tsx:
--------------------------------------------------------------------------------
1 | import { FullWidth } from "~/components/FullWidth";
2 | import { MotionSandbox } from "../MotionSandbox";
3 |
4 | const motionCode = `import React from 'react'
5 |
6 | export default function Motion() {
7 | const squareRef = React.useRef();
8 | const initialPositionRef = React.useRef();
9 |
10 | React.useLayoutEffect(() => {
11 | const box = squareRef.current?.getBoundingClientRect();
12 | if (moved(initialPositionRef.current, box)) {
13 | // get the difference in position
14 | const deltaX = initialPositionRef.current.x - box.x;
15 | const deltaY = initialPositionRef.current.y - box.y;
16 | console.log(deltaX, deltaY);
17 |
18 | // apply the transform to the box
19 | squareRef.current.style.transform = \`translate(\${deltaX}px, \${deltaY}px)\`;
20 | }
21 | initialPositionRef.current = box;
22 | });
23 |
24 | return
;
25 | }
26 |
27 | const moved = (initialBox, finalBox) => {
28 | // we just mounted, so we don't have complete data yet
29 | if (!initialBox || !finalBox) return false;
30 |
31 | const xMoved = initialBox.x !== finalBox.x;
32 | const yMoved = initialBox.y !== finalBox.y;
33 |
34 | return xMoved || yMoved;
35 | }
36 | `;
37 |
38 | export const InverseSandbox = () => {
39 | return (
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseSandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InverseSandbox'
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseScaleFormula/InverseScaleFormula.stories.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | InverseScaleFormula,
3 | InverseScaleFormulaSandbox,
4 | } from "./InverseScaleFormula";
5 |
6 | export const Default = () => (
7 |
8 | );
9 |
10 | export const Sandbox = () => ;
11 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseScaleFormula/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InverseScaleFormula'
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseScaleGraph/InverseScaleGraph.stories.tsx:
--------------------------------------------------------------------------------
1 | import { InverseScaleGraph } from './InverseScaleGraph'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseScaleGraph/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InverseScaleGraph'
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseScaleSandbox/InverseScaleSandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { InverseScaleSandbox } from './InverseScaleSandbox'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseScaleSandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InverseScaleSandbox'
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseSizeSlider/InverseSizeSlider.stories.tsx:
--------------------------------------------------------------------------------
1 | import { InverseSizeSlider } from './InverseSizeSlider'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseSizeSlider/InverseSizeSlider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { animate, useMotionValue } from "framer-motion";
3 |
4 | import { FullWidth } from "~/components/FullWidth";
5 | import {
6 | Visualizer,
7 | Content,
8 | Controls,
9 | PlayButton,
10 | UndoButton,
11 | } from "~/components/Visualizer";
12 | import { styled } from "~/stitches.config";
13 |
14 | import { Ruler, RulerWrapper, RulerText } from "../shared/Ruler";
15 | import { SizeDiagram } from "../shared/SizeDiagram";
16 |
17 | export const InverseSizeSlider = () => {
18 | const scale = useMotionValue(1);
19 | const [width, setWidth] = React.useState(0);
20 |
21 | return (
22 |
23 |
24 |
32 |
33 | 120px
34 |
35 |
36 |
37 |
38 |
39 |
40 | {width}px
41 |
42 |
43 |
44 | animate(scale, 120 / width, { duration: 3 })}
46 | />
47 | scale.set(1)} />
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | const OriginalSquare = styled("div", {
55 | width: 121,
56 | height: 120,
57 | borderRight: "1px dashed $blue8",
58 | position: "absolute",
59 | });
60 |
--------------------------------------------------------------------------------
/content/magic-motion/components/InverseSizeSlider/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InverseSizeSlider'
--------------------------------------------------------------------------------
/content/magic-motion/components/LayoutChangeExample/LayoutChangeExample.stories.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | JustifyContentExample,
3 | LayoutChangeExample,
4 | TransformExample,
5 | } from "./LayoutChangeExample";
6 |
7 | export const Default = () => ;
8 |
9 | export const JustifyContent = () => ;
10 |
11 | export const Transform = () => ;
12 |
--------------------------------------------------------------------------------
/content/magic-motion/components/LayoutChangeExample/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './LayoutChangeExample'
--------------------------------------------------------------------------------
/content/magic-motion/components/Motion/Motion.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Motion } from "./Motion";
3 |
4 | import { styled } from "~/stitches.config";
5 | import { GridBackground } from "~/components/Grid";
6 |
7 | export const Corrected = () => {
8 | const [toggled, setToggled] = React.useState(false);
9 | return (
10 | <>
11 | setToggled(!toggled)}>Toggle
12 |
13 |
14 |
15 |
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export const NaiveScaleCorrection = () => {
23 | const [toggled, setToggled] = React.useState(false);
24 | return (
25 | <>
26 | setToggled(!toggled)}>Toggle
27 |
28 |
29 |
37 | Hello
38 |
39 |
40 |
41 | >
42 | );
43 | };
44 |
45 | const Wrapper = styled("div", {
46 | padding: "$8",
47 | display: "flex",
48 | alignItems: "center",
49 | height: 300,
50 |
51 | variants: {
52 | toggled: {
53 | true: {
54 | justifyContent: "flex-end",
55 | },
56 | },
57 | center: {
58 | true: {
59 | justifyContent: "center",
60 | },
61 | },
62 | },
63 | });
64 |
65 | const Original = styled("div", {
66 | position: "absolute",
67 | width: 120,
68 | height: 120,
69 | background: "$gray6",
70 | left: "$8",
71 | });
72 |
--------------------------------------------------------------------------------
/content/magic-motion/components/Motion/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Motion'
--------------------------------------------------------------------------------
/content/magic-motion/components/MotionSandbox/MotionSandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { MotionSandbox } from "./MotionSandbox";
2 |
3 | const motionCode = `import React from 'react'
4 |
5 | export default function Motion() {
6 | const squareRef = React.useRef()
7 |
8 | React.useEffect(() => {
9 | const box = squareRef.current?.getBoundingClientRect()
10 | if (box) { console.log(box.x, box.y) }
11 | })
12 |
13 | return
14 | }`;
15 |
16 | export const Default = () => ;
17 |
--------------------------------------------------------------------------------
/content/magic-motion/components/MotionSandbox/MotionSandbox.tsx:
--------------------------------------------------------------------------------
1 | import { Sandbox } from "~/components/Sandbox";
2 |
3 | const css = `
4 | html {
5 | background: hsl(0, 0%, 13.6%);
6 | color: white;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | padding: 16px;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13 | }
14 |
15 | #motion {
16 | border: 1px solid hsl(208, 100%, 47.3%);
17 | background: hsl(206, 100%, 30%);
18 | width: 120px;
19 | aspect-ratio: 1;
20 | border-radius: 8px;
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | }
25 |
26 | #wrapper {
27 | margin-top: 8px;
28 | display: flex;
29 | }
30 | `;
31 |
32 | const code = `import React from 'react'
33 | import Motion from './Motion'
34 | import './styles.css'
35 |
36 | `;
37 |
38 | const defaultAppCode = `export default function App() {
39 | const [toggled, toggle] = React.useReducer(state => !state, false)
40 |
41 | return (
42 |
43 |
Toggle
44 |
45 |
46 |
47 |
48 | )
49 | }
50 | `;
51 |
52 | export const MotionSandbox = ({ motionCode, appCode = defaultAppCode }) => {
53 | return (
54 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/content/magic-motion/components/MotionSandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./MotionSandbox";
2 |
--------------------------------------------------------------------------------
/content/magic-motion/components/MotionSquare/MotionSquare.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | useMotionTemplate,
4 | useMotionValue,
5 | motion,
6 | useTransform,
7 | type MotionValue,
8 | } from "framer-motion";
9 | import { FullWidth } from "~/components/FullWidth";
10 | import { SvgGridWrapper } from "~/components/SvgGridWrapper";
11 | import { Slider } from "~/components/Slider";
12 |
13 | import { MotionSquare } from "./MotionSquare";
14 |
15 | const Wrapper = ({
16 | width,
17 | children,
18 | }: {
19 | width: MotionValue;
20 | children: React.ReactNode;
21 | }) => {
22 | const radius = useTransform(width, (width) => width / 2);
23 | const transform = useMotionTemplate`translate(calc(50% - ${radius}px), calc(50% - ${radius}px))`;
24 | return (
25 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export const Default = () => {
32 | const width = useMotionValue(120);
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export const WithChangingWidth = () => {
43 | const width = useMotionValue(120);
44 | return (
45 |
46 | {
52 | width.set(newWidth);
53 | }}
54 | />
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export const TopLeft = () => {
63 | const width = useMotionValue(120);
64 | return (
65 |
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export const WithChangingWidthTopLeft = () => {
74 | const width = useMotionValue(120);
75 | return (
76 |
77 | {
83 | width.set(newWidth);
84 | }}
85 | />
86 |
87 |
88 |
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/content/magic-motion/components/MotionSquare/MotionSquare.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MotionValue, useTransform } from "framer-motion";
3 |
4 | import { BaseSvgSquare } from "../shared/styles";
5 | import { LineEndpoint, Line } from "../shared/HorizontalRuler";
6 |
7 | export type MotionSquareProps = {
8 | width: MotionValue;
9 | showScaleRulers?: boolean;
10 | topLeft?: boolean;
11 | };
12 |
13 | export const MotionSquare = ({
14 | width,
15 | topLeft = false,
16 | showScaleRulers = false,
17 | }: MotionSquareProps) => {
18 | return (
19 |
20 |
21 | {showScaleRulers && }
22 |
23 | );
24 | };
25 |
26 | export const ScaleRulers = ({
27 | width,
28 | topLeft = false,
29 | }: {
30 | width: MotionValue;
31 | topLeft?: boolean;
32 | }) => {
33 | const lineRef = React.useRef();
34 | const radius = useTransform(width, (width) => width / 2);
35 |
36 | const initialWidth = width.get();
37 | const initialRadius = radius.get();
38 |
39 | React.useEffect(() => {
40 | return width.onChange((width) => {
41 | lineRef.current?.setAttribute("x2", width.toString());
42 | if (topLeft) {
43 | lineRef.current?.setAttribute("y2", width.toString());
44 | }
45 | });
46 | }, [width, topLeft]);
47 |
48 | React.useEffect(() => {
49 | return radius.onChange((radius) => {
50 | if (!topLeft) {
51 | lineRef.current?.setAttribute("x1", radius.toString());
52 | lineRef.current?.setAttribute("y1", radius.toString());
53 | }
54 | });
55 | }, [radius, topLeft]);
56 |
57 | return (
58 |
59 |
66 |
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/content/magic-motion/components/MotionSquare/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './MotionSquare'
--------------------------------------------------------------------------------
/content/magic-motion/components/PlaySandbox/PlaySandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { PlaySandbox } from './PlaySandbox'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/PlaySandbox/PlaySandbox.tsx:
--------------------------------------------------------------------------------
1 | import { FullWidth } from "~/components/FullWidth";
2 | import { MotionSandbox } from "../MotionSandbox";
3 |
4 | const motionCode = `import React from 'react'
5 | import { animate } from 'popmotion'
6 |
7 | export default function Motion() {
8 | const squareRef = React.useRef();
9 | const initialPositionRef = React.useRef();
10 |
11 | React.useLayoutEffect(() => {
12 | const box = squareRef.current?.getBoundingClientRect();
13 | if (moved(initialPositionRef.current, box)) {
14 | // get the difference in position
15 | const deltaX = initialPositionRef.current.x - box.x;
16 | const deltaY = initialPositionRef.current.y - box.y;
17 |
18 | // inverse the change using a transform
19 | squareRef.current.style.transform = \`translate(\${deltaX}px, \${deltaY}px)\`;
20 |
21 | // animate back to the final position
22 | animate({
23 | from: 1,
24 | to: 0,
25 | duration: 2000,
26 | onUpdate: progress => {
27 | squareRef.current.style.transform =
28 | \`translate(\${deltaX * progress}px, \${deltaY * progress}px)\`;
29 | }
30 | })
31 | }
32 | initialPositionRef.current = box;
33 | });
34 |
35 | return
;
36 | }
37 |
38 | const moved = (initialBox, finalBox) => {
39 | // we just mounted, so we don't have complete data yet
40 | if (!initialBox || !finalBox) return false;
41 |
42 | const xMoved = initialBox.x !== finalBox.x;
43 | const yMoved = initialBox.y !== finalBox.y;
44 |
45 | return xMoved || yMoved;
46 | }
47 | `;
48 |
49 | export const PlaySandbox = () => {
50 | return (
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/content/magic-motion/components/PlaySandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './PlaySandbox'
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceExample/SizeDistanceExample.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SizeDistanceExample } from "./SizeDistanceExample";
2 |
3 | export const Default = () => ;
4 |
5 | export const WithText = () => ;
6 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceExample/SizeDistanceExample.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { FullWidth } from "~/components/FullWidth";
4 | import {
5 | Visualizer,
6 | Content,
7 | Controls,
8 | ToggleButton,
9 | } from "~/components/Visualizer";
10 | import { styled } from "~/stitches.config";
11 |
12 | import { Motion } from "../Motion";
13 |
14 | export const SizeDistanceExample = ({ text = null, corrected = false }) => {
15 | const [toggled, setToggled] = React.useState(false);
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {text}
23 |
24 |
25 |
26 |
27 | setToggled(!toggled)}>
28 | Toggle
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export const NaiveScaleCorrection = () => {
37 | const [toggled, setToggled] = React.useState(false);
38 | return (
39 |
40 |
41 |
42 |
43 |
51 | Hello
52 |
53 |
54 |
55 |
56 | setToggled(!toggled)}>
57 | Toggle
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | const Wrapper = styled("div", {
66 | padding: "$8",
67 | display: "flex",
68 | alignItems: "center",
69 | height: 300,
70 |
71 | variants: {
72 | toggled: {
73 | true: {
74 | justifyContent: "flex-end",
75 | },
76 | },
77 | center: {
78 | true: {
79 | justifyContent: "center",
80 | },
81 | },
82 | },
83 | });
84 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceExample/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizeDistanceExample'
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceInverseSnapshot/SizeDistanceInverseSnapshot.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SizeDistanceInverseSnapshot } from './SizeDistanceInverseSnapshot'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceInverseSnapshot/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizeDistanceInverseSnapshot'
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceRelationship/SizeDistanceRelationship.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SizeDistanceRelationship } from './SizeDistanceRelationship'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeDistanceRelationship/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizeDistanceRelationship'
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeLayoutExample/SizeLayoutExample.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SizeLayoutExample } from './SizeLayoutExample'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeLayoutExample/SizeLayoutExample.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { GridBackground } from "~/components/Grid";
4 | import { FullWidth } from "~/components/FullWidth";
5 | import { ChangeIndicator } from "~/components/ChangeIndicator";
6 | import { ToggleButton } from "~/components/Visualizer";
7 |
8 | import { ContentWrapper, Controls, AlignmentText } from "../shared";
9 | import { SizeExample } from "../size";
10 |
11 | export const SizeLayoutExample = () => {
12 | const [toggled, toggle] = React.useReducer((state) => !state, false);
13 |
14 | return (
15 |
16 |
17 |
18 | Toggle Layout
19 |
20 |
21 | width:{" "}
22 |
23 | {toggled ? "100%" : "120px"}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeLayoutExample/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizeLayoutExample'
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeMeasurements/SizeMeasurements.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SizeMeasurements } from './SizeMeasurements'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizeMeasurements/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizeMeasurements'
--------------------------------------------------------------------------------
/content/magic-motion/components/SizePlayAnimation/SizePlayAnimation.stories.tsx:
--------------------------------------------------------------------------------
1 | import { SizePlayAnimation } from './SizePlayAnimation'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizePlayAnimation/SizePlayAnimation.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { animate, useMotionValue } from "framer-motion";
3 | import { FaUndo, FaPlay } from "react-icons/fa";
4 |
5 | import {
6 | Visualizer,
7 | Content,
8 | Controls,
9 | PlayButton,
10 | UndoButton,
11 | } from "~/components/Visualizer";
12 | import { FullWidth } from "~/components/FullWidth";
13 |
14 | import { SizeDiagram } from "../shared/SizeDiagram";
15 |
16 | export const SizePlayAnimation = () => {
17 | const [width, setWidth] = React.useState(0);
18 | const scale = useMotionValue(1);
19 | return (
20 |
21 |
22 |
23 | {
26 | setWidth(width);
27 | scale.set(120 / width);
28 | }}
29 | />
30 |
31 |
32 | animate(scale, 1, { duration: 3 })} />
33 | scale.set(120 / width)} />
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/content/magic-motion/components/SizePlayAnimation/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizePlayAnimation'
--------------------------------------------------------------------------------
/content/magic-motion/components/WidthTransitionSandbox/WidthTransitionSandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { WidthTransitionSandbox } from './WidthTransitionSandbox'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/WidthTransitionSandbox/WidthTransitionSandbox.tsx:
--------------------------------------------------------------------------------
1 | import { Sandbox } from "~/components/Sandbox";
2 |
3 | const css = `
4 | html {
5 | background: #151515;
6 | }
7 |
8 | body {
9 | margin: 0;
10 | padding: 0;
11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
12 | }
13 |
14 | .square {
15 | border: 1px solid hsl(0, 0%, 24.3%);
16 | background: hsl(0, 0%, 15.8%);
17 | width: 120px;
18 | aspect-ratio: 1;
19 | border-radius: 8px;
20 | }
21 |
22 | #wrapper {
23 | margin-top: 8px;
24 | display: flex;
25 | gap: 8px;
26 | }
27 | `;
28 |
29 | const styles = `.active {
30 | border: 1px solid hsl(208, 100%, 47.3%);
31 | background: hsl(206, 100%, 50%);
32 | width: 120px;
33 | height: 120px;
34 | border-radius: 8px;
35 | transition: width 0.5s ease-out;
36 | }
37 |
38 | .toggled {
39 | width: 200px;
40 | }
41 | `;
42 |
43 | const code = `import React from 'react'
44 | import Motion from './Motion'
45 | import './main.css'
46 |
47 | `;
48 |
49 | const defaultAppCode = `export default function App() {
50 | const [toggled, toggle] = React.useReducer(state => !state, false)
51 |
52 | return (
53 |
61 | )
62 | }
63 | `;
64 |
65 | const motionCode = `import React from 'react'
66 | import './styles.css'
67 |
68 | export default function Motion({ toggled }) {
69 | return
70 | }`;
71 |
72 | export const WidthTransitionSandbox = () => {
73 | return (
74 |
91 | );
92 | };
93 |
--------------------------------------------------------------------------------
/content/magic-motion/components/WidthTransitionSandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './WidthTransitionSandbox'
--------------------------------------------------------------------------------
/content/magic-motion/components/shared/HorizontalRuler/HorizontalRuler.stories.tsx:
--------------------------------------------------------------------------------
1 | import { HorizontalRuler } from "./HorizontalRuler";
2 |
3 | export const Default = () => ;
4 |
--------------------------------------------------------------------------------
/content/magic-motion/components/shared/HorizontalRuler/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './HorizontalRuler'
--------------------------------------------------------------------------------
/content/magic-motion/components/shared/Ruler.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 |
4 | import { styled, keyframes } from "~/stitches.config";
5 |
6 | export const Ruler = () => {
7 | const [toggled, setToggled] = React.useState(false);
8 |
9 | React.useEffect(() => {
10 | const timeout = setTimeout(() => setToggled(true), 100);
11 | return () => clearTimeout(timeout);
12 | }, []);
13 |
14 | return (
15 |
16 |
17 |
18 |
23 |
24 | );
25 | };
26 |
27 | export const RulerWrapper = styled("div", {
28 | width: 120,
29 | display: "flex",
30 | alignItems: "center",
31 | flexDirection: "column",
32 | position: "absolute",
33 |
34 | variants: {
35 | full: {
36 | true: {
37 | width: "calc(100% - $space$8 * 2)",
38 | },
39 | },
40 | },
41 | });
42 |
43 | const fadeIn = keyframes({
44 | from: {
45 | opacity: 0,
46 | },
47 | to: {
48 | opacity: 1,
49 | },
50 | });
51 |
52 | export const RulerText = styled(motion.p, {
53 | fontFamily: "$mono",
54 | color: "$gray11",
55 | fontSize: "$sm",
56 | opacity: 0,
57 | animationName: `${fadeIn}`,
58 | animationDuration: "500ms",
59 | animationFillMode: "forwards",
60 | animationTimingFunction: "ease-out",
61 | });
62 |
63 | const Line = styled(motion.div, {
64 | height: 2,
65 | width: "100%",
66 | background: "$gray9",
67 | position: "absolute",
68 | top: 4,
69 | });
70 |
71 | const Bar = styled(motion.div, {
72 | height: 10,
73 | width: 2,
74 | background: "$gray9",
75 | });
76 |
77 | const Measurement = styled("div", {
78 | display: "flex",
79 | justifyContent: "center",
80 | position: "relative",
81 | width: "100%",
82 |
83 | variants: {
84 | toggled: {
85 | true: {
86 | justifyContent: "space-between",
87 | },
88 | },
89 | },
90 | });
91 |
--------------------------------------------------------------------------------
/content/magic-motion/components/shared/SizeDiagram/SizeDiagram.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useMotionValue } from "framer-motion";
2 | import { SizeDiagram } from "./SizeDiagram";
3 |
4 | export const Default = () => {
5 | const scale = useMotionValue(0.5);
6 | return ;
7 | };
8 |
9 | export const WithWidthChange = () => {
10 | const scale = useMotionValue(0.5);
11 | return (
12 | scale.set(120 / width)}
15 | />
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/content/magic-motion/components/shared/SizeDiagram/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './SizeDiagram'
--------------------------------------------------------------------------------
/content/magic-motion/components/shared/styles.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ComponentPropsWithoutRef } from "react";
2 | import { motion } from "framer-motion";
3 |
4 | import { styled, darkTheme } from "~/stitches.config";
5 |
6 | export const PADDING = 32;
7 | export const SQUARE_RADIUS = 60;
8 |
9 | const _BaseSvgSquare = styled(motion.rect, {
10 | filter: "drop-shadow($shadows$sm)",
11 | fill: "$blue6",
12 | stroke: "$blue8",
13 |
14 | [`.${darkTheme} &`]: {
15 | fill: "$blueDark8",
16 | stroke: "$blueDark10",
17 | },
18 |
19 | variants: {
20 | type: {
21 | secondary: {
22 | fill: "$gray6",
23 | stroke: "$gray8",
24 | filter: "none",
25 |
26 | [`.${darkTheme} &`]: {
27 | fill: "$gray4",
28 | stroke: "$gray6",
29 | },
30 | },
31 | },
32 | },
33 | });
34 |
35 | export const BaseSvgSquare = React.forwardRef<
36 | SVGRectElement,
37 | ComponentPropsWithoutRef
38 | >(function BaseSvgSquare(props, ref) {
39 | return <_BaseSvgSquare ref={ref} rx="6" {...props} />;
40 | });
41 |
42 | export const SvgSquare = styled(BaseSvgSquare, {
43 | height: SQUARE_RADIUS * 2,
44 | });
45 |
--------------------------------------------------------------------------------
/content/magic-motion/components/size.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 |
4 | import { styled } from "~/stitches.config";
5 | import { Square } from "./shared";
6 |
7 | export const SizeExample = React.forwardRef<
8 | HTMLButtonElement,
9 | { toggled: boolean; layout?: boolean }
10 | >(function SizeExample({ toggled, layout = true }, ref) {
11 | return (
12 | <>
13 | {layout && (
14 |
20 | )}
21 |
29 | >
30 | );
31 | });
32 |
33 | const FakeBorder = styled(motion.div, {
34 | position: "absolute",
35 | left: `calc($space$8 - 1px)`,
36 | background: "$blue8",
37 | width: 122,
38 | height: 122,
39 |
40 | "@md": {
41 | left: `calc($space$12 - 1px)`,
42 | },
43 |
44 | variants: {
45 | toggled: {
46 | true: {
47 | width: "calc(100% - $space$12 * 2 + 2px)",
48 | },
49 | },
50 | },
51 | });
52 |
53 | const DisplayOnlySquare = styled(Square, {
54 | position: "relative",
55 | pointerEvents: "none",
56 | height: 120,
57 |
58 | variants: {
59 | toggled: {
60 | true: {
61 | width: "100%",
62 | aspectRatio: "auto",
63 | },
64 | },
65 | noBorder: {
66 | true: {
67 | border: "none",
68 | },
69 | },
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/content/tokenizer/components/Boilerplate.tsx:
--------------------------------------------------------------------------------
1 | import { useAlgorithm } from "~/lib/algorithm";
2 | import { styled } from "~/stitches.config";
3 | import snapshot from "~/lib/algorithm/snapshot.macro";
4 | import { GridBackground } from "~/components/Grid";
5 | import { PlayButton } from "~/components/PlayButton";
6 |
7 | import type { Token } from "./lib/tokenize";
8 | import { CharacterList } from "./CharacterList";
9 |
10 | const boilerplate = snapshot(function tokenize(input: string) {
11 | let current = 0;
12 | let tokens: Token[] = [];
13 |
14 | while (current < input.length) {
15 | // parse tokens
16 | debugger;
17 | current++;
18 | }
19 |
20 | debugger;
21 | return tokens;
22 | });
23 |
24 | const input = `function hello() {
25 | console.log('hello, world!')
26 | }`;
27 |
28 | type BoilerplateState = {
29 | input: string;
30 | current: number;
31 | tokens: Token[];
32 | };
33 |
34 | export function Boilerplate() {
35 | const [state, ctx] = useAlgorithm(boilerplate, [input], {
36 | delay: 200,
37 | });
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | const Wrapper = styled("div", {
51 | display: "flex",
52 | flexDirection: "column",
53 | justifyContent: "center",
54 | alignItems: "center",
55 | padding: "$12 0",
56 | });
57 |
58 | const ControlsWrapper = styled("div", {
59 | position: "absolute",
60 | left: "$2",
61 | bottom: "$2",
62 | });
63 |
--------------------------------------------------------------------------------
/content/tokenizer/components/CharacterList.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { darkTheme, styled } from "~/stitches.config";
3 |
4 | type CharacterListProps = {
5 | state: {
6 | input: string;
7 | current: number;
8 | };
9 | };
10 |
11 | export function CharacterList({ state }: CharacterListProps) {
12 | return (
13 |
14 |
18 | {[...state.input].map((char, index) => {
19 | const isActive = index === state.current;
20 | return (
21 |
26 | {char === "\n" ? `\\n` : char}
27 |
28 | );
29 | })}
30 |
31 |
32 | );
33 | }
34 |
35 | const Wrapper = styled("div", {
36 | width: "100%",
37 | position: "relative",
38 | height: "2.5rem",
39 | overflowX: "hidden",
40 | });
41 |
42 | const InputWrapper = styled(motion.div, {
43 | fontFamily: "$mono",
44 | display: "flex",
45 | position: "absolute",
46 | left: "50%",
47 | });
48 |
49 | const InputCharacter = styled("p", {
50 | color: "$gray9",
51 | position: "relative",
52 | variants: {
53 | type: {
54 | empty: {
55 | // TODO: Compute this based on font size and line height
56 | width: 12.3,
57 | },
58 | },
59 | active: {
60 | true: {
61 | color: "$gray1",
62 | background: "$gray12",
63 |
64 | [`.${darkTheme} &`]: {
65 | background: "$gray10",
66 | },
67 | },
68 | },
69 | },
70 | });
71 |
--------------------------------------------------------------------------------
/content/tokenizer/components/Pipeline.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { FaArrowRight } from "react-icons/fa";
3 |
4 | import { GridOverflowBox } from "~/components/Grid";
5 | import { styled } from "~/stitches.config";
6 |
7 | const phases = ["Tokenize", "Parse", "Transform", "Generate"];
8 |
9 | export const Pipeline = () => {
10 | return (
11 |
12 |
13 | {phases.map((phase) => (
14 | <>
15 |
16 |
17 |
18 | {phase}
19 | >
20 | ))}
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | const IconWrapper = styled("li", {
30 | color: "$gray11",
31 | display: "flex",
32 | alignItems: "center",
33 | });
34 |
35 | const PhaseList = styled("ul", {
36 | listStyle: "none",
37 | gap: "$2",
38 | display: "grid",
39 | gridTemplateColumns: "repeat(4, $space$4 1fr) $space$4",
40 | alignItems: "center",
41 | });
42 |
43 | const Phase = styled(motion.li, {
44 | border: "1px solid $gray8",
45 | padding: "$3",
46 | borderRadius: "$base",
47 | background: "$gray2",
48 | fontSize: "$sm",
49 | fontFamily: "$mono",
50 | textAlign: "center",
51 | });
52 |
--------------------------------------------------------------------------------
/content/tokenizer/components/Tokenizer/Tokenizer.stories.tsx:
--------------------------------------------------------------------------------
1 | import { FullWidth as Wrapper } from "~/components/FullWidth";
2 | import { Tokenizer } from "./Tokenizer";
3 |
4 | export const Default = () => ;
5 |
6 | export const FullWidth = () => (
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/content/tokenizer/components/Tokenizer/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Tokenizer";
2 |
--------------------------------------------------------------------------------
/content/tokenizer/components/lib/single-character.ts:
--------------------------------------------------------------------------------
1 | import { knownSingleCharacters } from "./tokenize";
2 | import snapshot from "~/lib/algorithm/snapshot.macro";
3 |
4 | export const singleCharacter = snapshot(function tokenize(input) {
5 | let phase = "Starting... ⚙️";
6 | let current = 0;
7 | let tokens = [];
8 |
9 | debugger;
10 |
11 | while (current < input.length) {
12 | const currentChar = input[current];
13 |
14 | const builder = knownSingleCharacters.get(currentChar);
15 | if (builder) {
16 | const phase = "Known Token 📕";
17 | debugger;
18 | tokens.push(builder());
19 | debugger;
20 | current++;
21 | } else {
22 | const phase = "Skipping... 🧹";
23 | debugger;
24 | current++;
25 | }
26 | }
27 |
28 | phase = "Done! ✨";
29 | debugger;
30 | return tokens;
31 | });
32 |
--------------------------------------------------------------------------------
/experiments/BezierCurveQuiz/BezierCurveQuiz.stories.tsx:
--------------------------------------------------------------------------------
1 | import { BezierCurveQuiz } from './BezierCurveQuiz'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/experiments/BezierCurveQuiz/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './BezierCurveQuiz'
--------------------------------------------------------------------------------
/experiments/Cards/Cards.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Cards } from './Cards'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/experiments/Cards/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Cards'
--------------------------------------------------------------------------------
/experiments/ControllableAnimation/ControllableAnimation.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ControllableAnimation } from './ControllableAnimation'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/experiments/ControllableAnimation/ControllableAnimation.tsx:
--------------------------------------------------------------------------------
1 | import { animate, motion, useMotionValue, useTransform } from "framer-motion";
2 | import { Slider } from "~/components/Slider";
3 | import {
4 | Visualizer,
5 | Content,
6 | Controls,
7 | ToggleButton,
8 | PlayButton,
9 | } from "~/components/Visualizer";
10 | import { styled } from "~/stitches.config";
11 |
12 | export const ControllableAnimation = () => {
13 | const progress = useMotionValue(0);
14 | const y = useTransform(progress, [0, 1], [400, 0], {
15 | ease: (x) => Math.sin((x * Math.PI) / 2),
16 | });
17 |
18 | const animateProgress = (duration = 0.5) => {
19 | progress.set(0);
20 | animate(progress, 1, { duration, ease: "linear" });
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 | - Hello world!
28 |
29 |
30 |
31 | animateProgress()}
34 | />
35 | progress.set(value / 100)}
39 | />
40 |
41 | 0.1x
42 |
43 | 0.5x
44 | 1x
45 | 2x
46 |
47 |
48 | );
49 | };
50 |
51 | const Button = styled(ToggleButton, {
52 | fontWeight: "bold",
53 | color: "$gray11",
54 | });
55 |
56 | const File = styled("ul", {
57 | border: "1px solid $gray8",
58 | boxShadow: "$md",
59 | borderRadius: "$base",
60 | height: 400,
61 | width: 300,
62 | margin: "0 auto",
63 | background: "linear-gradient(45deg, $gray5, $gray4)",
64 | listStyle: "none",
65 | padding: "$6 0",
66 | });
67 |
68 | const Item = styled(motion.li, {
69 | margin: "0 -$4",
70 | padding: "$4",
71 | background: "linear-gradient(45deg, $gray3, $gray2)",
72 | borderRadius: 8,
73 | border: "1px solid $gray8",
74 | boxShadow: "$sm",
75 | textAlign: "center",
76 | fontFamily: "$mono",
77 | });
78 |
--------------------------------------------------------------------------------
/experiments/ControllableAnimation/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './ControllableAnimation'
--------------------------------------------------------------------------------
/experiments/EasingCurveEditor/EasingCurveEditor.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { EasingCurveEditor, type CubicBezier } from "./EasingCurveEditor";
3 |
4 | export const Default = () => {
5 | const [easing, setEasing] = React.useState([0.25, 0.1, 0.25, 1]);
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/experiments/EasingCurveEditor/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './EasingCurveEditor'
--------------------------------------------------------------------------------
/experiments/EasingFunctionSandbox/EasingFunctionSandbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { EasingFunctionSandbox } from './EasingFunctionSandbox'
2 |
3 | export const Default = () =>
4 |
--------------------------------------------------------------------------------
/experiments/EasingFunctionSandbox/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './EasingFunctionSandbox'
--------------------------------------------------------------------------------
/experiments/TextLoadAnimation/TextLoadAnimation.stories.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | TextLoadAnimation,
3 | TextLoadAnimationWindow,
4 | } from "./TextLoadAnimation";
5 |
6 | export const Default = () => ;
7 |
8 | export const Window = () => ;
9 |
--------------------------------------------------------------------------------
/experiments/TextLoadAnimation/alphanumeric.tsx:
--------------------------------------------------------------------------------
1 | export const alphanumericChars = [
2 | "0",
3 | "1",
4 | "2",
5 | "3",
6 | "4",
7 | "5",
8 | "6",
9 | "7",
10 | "8",
11 | "9",
12 | "a",
13 | "b",
14 | "c",
15 | "d",
16 | "e",
17 | "f",
18 | "g",
19 | "h",
20 | "i",
21 | "j",
22 | "k",
23 | "l",
24 | "m",
25 | "n",
26 | "o",
27 | "p",
28 | "q",
29 | "r",
30 | "s",
31 | "t",
32 | "u",
33 | "v",
34 | "w",
35 | "x",
36 | "y",
37 | "z",
38 | "A",
39 | "B",
40 | "C",
41 | "D",
42 | "E",
43 | "F",
44 | "G",
45 | "H",
46 | "I",
47 | "J",
48 | "K",
49 | "L",
50 | "M",
51 | "N",
52 | "O",
53 | "P",
54 | "Q",
55 | "R",
56 | "S",
57 | "T",
58 | "U",
59 | "V",
60 | "W",
61 | "X",
62 | "Y",
63 | "Z",
64 | "@",
65 | "#",
66 | "$",
67 | "%",
68 | "&",
69 | "*",
70 | "+",
71 | "-",
72 | ".",
73 | "/",
74 | ":",
75 | ";",
76 | "<",
77 | "=",
78 | ">",
79 | "?",
80 | "[",
81 | "]",
82 | "{",
83 | "}",
84 | "(",
85 | ")",
86 | ];
87 |
--------------------------------------------------------------------------------
/experiments/TextLoadAnimation/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './TextLoadAnimation'
--------------------------------------------------------------------------------
/experiments/TopDownParser/TopDownParser.stories.tsx:
--------------------------------------------------------------------------------
1 | import { TopDownParser } from './TopDownParser'
2 | export const Default = () =>
3 |
--------------------------------------------------------------------------------
/experiments/TopDownParser/TopDownParser.tsx:
--------------------------------------------------------------------------------
1 | export const TopDownParser = () => { return null; }
--------------------------------------------------------------------------------
/experiments/TopDownParser/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './TopDownParser'
--------------------------------------------------------------------------------
/lib/algorithm/exec.ts:
--------------------------------------------------------------------------------
1 | import rfdc from "rfdc";
2 |
3 | import type { Pushable, StateMetadata } from "./types";
4 |
5 | const clone = rfdc();
6 |
7 | export function exec<
8 | StateType,
9 | ParameterType extends unknown[],
10 | ReturnValueType
11 | >(
12 | algorithm: (
13 | snapshotter: Pushable
14 | ) => (...args: ParameterType) => ReturnValueType,
15 | inputs: ParameterType
16 | ) {
17 | const snapshots = [] as Array>;
18 | const returnVal = algorithm({
19 | push: (state: StateType) => snapshots.push(clone(state)),
20 | })(...inputs);
21 |
22 | const last = snapshots[snapshots.length - 1];
23 | if (last) {
24 | last.__done = true;
25 | last.__returnValue = returnVal;
26 | }
27 |
28 | return snapshots;
29 | }
30 |
--------------------------------------------------------------------------------
/lib/algorithm/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./exec";
2 | export * from "./use-algorithm";
3 |
--------------------------------------------------------------------------------
/lib/algorithm/snapshot.macro.d.ts:
--------------------------------------------------------------------------------
1 | import type { Fn, SnapshottedAlgorithm } from "./types";
2 |
3 | export default function snapshot(
4 | algorithm: Algorithm
5 | ): SnapshottedAlgorithm;
6 |
--------------------------------------------------------------------------------
/lib/algorithm/types.ts:
--------------------------------------------------------------------------------
1 | export type Pushable = {
2 | push(state: unknown): void;
3 | };
4 |
5 | export type Fn = {
6 | (...args: any[]): any;
7 | };
8 |
9 | export type AlgorithmOptions = {
10 | delay: number;
11 | loop: boolean;
12 | };
13 |
14 | export type AlgorithmContext = {
15 | currentStep: number;
16 | totalSteps: number;
17 | isPlaying: boolean;
18 | next(): void;
19 | prev(): void;
20 | reset(): void;
21 | toggle(): void;
22 | goTo(step: number): void;
23 | };
24 |
25 | export type SnapshottedAlgorithm = {
26 | entryPoint: (snapshots: Pushable) => Algorithm;
27 | params: string;
28 | code: string;
29 | };
30 |
31 | export type StateMetadata = {
32 | __done?: boolean;
33 | __returnValue?: ReturnType;
34 | };
35 |
--------------------------------------------------------------------------------
/lib/config.ts:
--------------------------------------------------------------------------------
1 | export const BASE_URL = "https://nan.fyi";
2 |
--------------------------------------------------------------------------------
/lib/highlighter.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as shiki from "shiki";
3 |
4 | const getHighlighter = async () => {
5 | const theme = await fetch(`/shiki/dark-default.json`).then((res) =>
6 | res.json()
7 | );
8 | const wasm = await fetch(`/shiki/onig.wasm`).then((res) => res.arrayBuffer());
9 | shiki.setWasm(wasm);
10 | shiki.setCDN("/shiki/");
11 | return shiki.getHighlighter({
12 | theme,
13 | langs: ["js", "ts", "jsx", "tsx", "html"],
14 | });
15 | };
16 |
17 | let cachedHighlighter: shiki.Highlighter | null = null;
18 |
19 | export const useHighlighter = (): shiki.Highlighter | null => {
20 | const [_highlighter, setHighlighter] =
21 | React.useState(() => cachedHighlighter);
22 |
23 | React.useEffect(() => {
24 | if (!_highlighter) {
25 | getHighlighter().then((highlighter) => {
26 | cachedHighlighter = highlighter;
27 | setHighlighter(highlighter);
28 | });
29 | }
30 | }, [_highlighter]);
31 |
32 | return _highlighter;
33 | };
34 |
--------------------------------------------------------------------------------
/lib/use-interval.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function useInterval(
4 | callback: () => void,
5 | { delay, immediate = false }: { delay: number; immediate?: boolean }
6 | ) {
7 | const intervalRef = React.useRef(null);
8 | const savedCallback = React.useRef(callback);
9 |
10 | React.useEffect(() => {
11 | savedCallback.current = callback;
12 | }, [callback]);
13 |
14 | React.useEffect(() => {
15 | const tick = () => savedCallback.current();
16 | if (typeof delay === "number") {
17 | intervalRef.current = window.setInterval(tick, delay);
18 | if (immediate) tick();
19 | return () => window.clearInterval(intervalRef.current);
20 | }
21 | }, [delay, immediate]);
22 |
23 | return intervalRef;
24 | }
25 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | export const range = (length: number) => [...Array(length).keys()];
2 |
3 | export const steppedRange = (start: number, end: number, step: number) => {
4 | let arr = [];
5 | for (let i = start; i <= end; i += step) {
6 | arr.push(i);
7 | }
8 | return arr;
9 | };
10 |
11 | export const getId = (text: string) => {
12 | return text?.toLowerCase?.().replace(/\s/g, "-").replace(/\.|\?/g, "");
13 | };
14 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | experimental: {
6 | forceSwcTransforms: true,
7 | appDir: true,
8 | },
9 | images: {
10 | domains: ["avatars.githubusercontent.com"],
11 | },
12 | async redirects() {
13 | return [
14 | {
15 | source: "/how-arrays-work",
16 | destination: "https://nan-archive.vercel.app/how-arrays-work",
17 | permanent: false,
18 | },
19 | {
20 | source: "/debugger",
21 | destination: "https://nan-archive.vercel.app/debugger",
22 | permanent: false,
23 | },
24 | {
25 | source: "/sliding-window",
26 | destination: "https://nan-archive.vercel.app/sliding-window",
27 | permanent: false,
28 | },
29 | ];
30 | },
31 | };
32 |
33 | module.exports = nextConfig;
34 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "next-themes";
2 | import { Analytics } from "@vercel/analytics/react";
3 |
4 | import { ThemeToggle } from "~/components/ThemeToggle";
5 | import { styled, darkTheme } from "~/stitches.config";
6 | import "../styles/fonts.css";
7 |
8 | function MyApp({ Component, pageProps }) {
9 | return (
10 | <>
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | >
26 | );
27 | }
28 |
29 | const ThemeToggleWrapper = styled("div", {
30 | display: "block",
31 | position: "fixed",
32 | top: "$4",
33 | right: "$4",
34 | zIndex: 10,
35 | borderRadius: 6,
36 | background: "$gray6",
37 |
38 | [`.${darkTheme} &`]: {
39 | background: "$gray2",
40 | },
41 | });
42 |
43 | const Background = styled("div", {
44 | backgroundImage: "url(/noise.png)",
45 | position: "fixed",
46 | inset: 0,
47 | mixBlendMode: "hard-light",
48 | pointerEvents: "none",
49 |
50 | [`.${darkTheme} &`]: {
51 | display: "none",
52 | },
53 | });
54 |
55 | const Wrapper = styled("div", {
56 | color: "$gray12",
57 | minHeight: "100vh",
58 |
59 | [`.${darkTheme} &`]: {
60 | background: "$gray1",
61 | },
62 | });
63 |
64 | export default MyApp;
65 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import NextDocument, { Html, Head, Main, NextScript } from "next/document";
2 | import { getCssText, globalStyles } from "~/stitches.config";
3 |
4 | export default class Document extends NextDocument {
5 | render() {
6 | globalStyles();
7 | return (
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/pages/_stories/[name].tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticPaths, GetStaticProps } from "next";
2 |
3 | import StoriesPage from "~/components/layout/StoriesPage";
4 | import { stories } from "../../stories.meta";
5 |
6 | export const getStaticProps: GetStaticProps = async (context) => {
7 | if (process.env.NODE_ENV === "production") {
8 | return { notFound: true };
9 | }
10 | return {
11 | props: {
12 | name: context.params?.name as string,
13 | },
14 | };
15 | };
16 |
17 | export const getStaticPaths: GetStaticPaths = async () => {
18 | // When this is true (in preview environments) don't
19 | // prerender any static pages
20 | // (faster builds, but slower initial page load)
21 | if (process.env.SKIP_BUILD_STATIC_GENERATION) {
22 | return {
23 | paths: [],
24 | fallback: "blocking",
25 | };
26 | }
27 | const names = stories.flatMap((group) =>
28 | group.stories.map((story) => story.name)
29 | );
30 | return {
31 | paths: names.map((name) => ({ params: { name } })),
32 | fallback: false,
33 | };
34 | };
35 |
36 | export default function StoriesIndex({ name }) {
37 | return ;
38 | }
39 |
--------------------------------------------------------------------------------
/pages/_stories/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps } from "next";
2 |
3 | import StoriesPage from "~/components/layout/StoriesPage";
4 | import { stories } from "../../stories.meta";
5 |
6 | export default function StoriesIndex() {
7 | return ;
8 | }
9 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import GithubProvider from "next-auth/providers/github";
3 |
4 | export default NextAuth({
5 | providers: [
6 | GithubProvider({
7 | clientId: process.env.GITHUB_ID,
8 | clientSecret: process.env.GITHUB_SECRET,
9 | profile(profile) {
10 | return {
11 | id: profile.id,
12 | name: profile.name,
13 | username: profile.login,
14 | email: profile.email,
15 | avatar: profile.avatar_url,
16 | };
17 | },
18 | }),
19 | ],
20 | callbacks: {
21 | jwt: async ({ token, account, user }) => {
22 | if (user) {
23 | // @ts-ignore
24 | token.picture = user.avatar;
25 | // @ts-ignore
26 | token.username = user.username;
27 | }
28 | if (account) {
29 | token.access_token = account.access_token;
30 | }
31 | return token;
32 | },
33 | session: async ({ session, token }) => {
34 | // @ts-ignore
35 | session.user.username = token.username;
36 | // @ts-ignore
37 | session.access_token = token.access_token;
38 | return session;
39 | },
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/pages/api/subscribe.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default async function handler(
4 | req: NextApiRequest,
5 | res: NextApiResponse
6 | ) {
7 | const { email } = req.query;
8 |
9 | const response = await fetch(`https://api.buttondown.email/v1/subscribers`, {
10 | method: "POST",
11 | headers: {
12 | Authorization: `Token ${process.env.BUTTON_DOWN_API_KEY}`,
13 | "Content-Type": "application/json",
14 | },
15 | body: JSON.stringify({ email }),
16 | });
17 |
18 | if (response.ok) {
19 | return res.status(201).end();
20 | }
21 |
22 | const { email: errors } = await response.json();
23 | return res.status(400).json({ errors });
24 | }
25 |
--------------------------------------------------------------------------------
/pages/experiments/bezier-curves.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import produce from "immer";
3 | import {
4 | ExperimentsPage,
5 | ExperimentWrapper,
6 | Controls,
7 | DebugField,
8 | } from "~/components/layout/ExperimentsPage";
9 | import { BezierCurveQuiz } from "~/experiments/BezierCurveQuiz";
10 | import { ButtonSlider } from "~/experiments/EasingFunctionSandbox";
11 | import { Row } from "~/components/layout/Row";
12 | import type { CubicBezier } from "~/experiments/EasingCurveEditor";
13 |
14 | export default function BezierCurvesPage() {
15 | const [key, setKey] = React.useState(0);
16 | const [easing, setEasing] = React.useState([0.62, 0, 0.18, 1]);
17 | const [debug, setDebug] = React.useState(false);
18 |
19 | const handleEasingPan = (index: number) => (value: number) => {
20 | if (!(index % 2)) {
21 | value = Math.max(0, Math.min(1, value));
22 | }
23 | if (value === easing[index]) return;
24 | setEasing((_easing) =>
25 | produce(_easing, (draft) => {
26 | draft[index] = value;
27 | })
28 | );
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
36 | setKey(key + 1)}
40 | >
41 |
42 | Easing
43 |
44 | {easing.map((value, index) => {
45 | return (
46 |
58 | );
59 | })}
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/pages/experiments/cards.tsx:
--------------------------------------------------------------------------------
1 | import { Cards } from "~/experiments/Cards";
2 | import {
3 | ExperimentsPage,
4 | ExperimentWrapper,
5 | } from "~/components/layout/ExperimentsPage";
6 |
7 | export default function CardsPage() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/bg-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/pp-editorial-new/PPEditorialNew-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/fonts/pp-editorial-new/PPEditorialNew-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/pp-editorial-new/PPEditorialNew-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/fonts/pp-editorial-new/PPEditorialNew-Bold.woff2
--------------------------------------------------------------------------------
/public/fonts/pp-editorial-new/PPEditorialNew-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/fonts/pp-editorial-new/PPEditorialNew-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/pp-editorial-new/PPEditorialNew-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/fonts/pp-editorial-new/PPEditorialNew-Regular.woff2
--------------------------------------------------------------------------------
/public/grid-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/grid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/noise.png
--------------------------------------------------------------------------------
/public/og/debugger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/debugger.png
--------------------------------------------------------------------------------
/public/og/how-arrays-work.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/how-arrays-work.png
--------------------------------------------------------------------------------
/public/og/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/index.png
--------------------------------------------------------------------------------
/public/og/keys-in-framer-motion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/keys-in-framer-motion.png
--------------------------------------------------------------------------------
/public/og/magic-motion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/magic-motion.png
--------------------------------------------------------------------------------
/public/og/sliding-window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/sliding-window.png
--------------------------------------------------------------------------------
/public/og/tokenizer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/og/tokenizer.png
--------------------------------------------------------------------------------
/public/shiki/onig.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandanmen/NotANumber/6407ce11d8284491938e85ab73a9c85b5818663d/public/shiki/onig.wasm
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/styles/fonts.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;0,800;0,900;1,400;1,700&display=swap");
2 | @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
3 |
4 | @font-face {
5 | font-family: "PP Editorial New";
6 | font-weight: 400;
7 | src: url("/fonts/pp-editorial-new/PPEditorialNew-Regular.woff2")
8 | format("woff2"),
9 | url("/fonts/pp-editorial-new/PPEditorialNew-Regular.woff") format("woff");
10 | }
11 |
12 | @font-face {
13 | font-family: "PP Editorial New";
14 | font-weight: 700;
15 | src: url("/fonts/pp-editorial-new/PPEditorialNew-Bold.woff2") format("woff2"),
16 | url("/fonts/pp-editorial-new/PPEditorialNew-Bold.woff") format("woff");
17 | }
18 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { gray, blue, green, red, yellow, cyan } = require("@radix-ui/colors");
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: ["./app/**/*.{js,ts,jsx,tsx,mdx}"],
6 | theme: {
7 | extend: {
8 | colors: {
9 | ...gray,
10 | ...blue,
11 | ...green,
12 | ...red,
13 | ...yellow,
14 | ...cyan,
15 | },
16 | fontFamily: {
17 | serif: "var(--font-serif)",
18 | sans: "var(--font-sans)",
19 | },
20 | screens: {
21 | coffee: "1370px",
22 | },
23 | },
24 | },
25 | plugins: [],
26 | };
27 |
--------------------------------------------------------------------------------
/tsconfig-esbuild.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "incremental": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "react-jsx",
21 | "baseUrl": ".",
22 | "paths": {
23 | "~/stitches.config": [
24 | "./stitches.config"
25 | ],
26 | "~/lib/*": [
27 | "./lib/*"
28 | ],
29 | "~/components/*": [
30 | "./components/*"
31 | ],
32 | "~/experiments/*": [
33 | "./experiments/*"
34 | ]
35 | }
36 | },
37 | "include": [
38 | "next-env.d.ts",
39 | "**/*.ts",
40 | "**/*.tsx"
41 | ],
42 | "exclude": [
43 | "node_modules"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "incremental": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "preserve",
21 | "baseUrl": ".",
22 | "paths": {
23 | "~/stitches.config": [
24 | "./stitches.config"
25 | ],
26 | "~/lib/*": [
27 | "./lib/*"
28 | ],
29 | "~/components/*": [
30 | "./components/*"
31 | ],
32 | "~/experiments/*": [
33 | "./experiments/*"
34 | ]
35 | },
36 | "plugins": [
37 | {
38 | "name": "next"
39 | }
40 | ],
41 | "strictNullChecks": false
42 | },
43 | "include": [
44 | "next-env.d.ts",
45 | "**/*.ts",
46 | "**/*.tsx",
47 | ".next/types/**/*.ts"
48 | ],
49 | "exclude": [
50 | "node_modules"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------