├── .eslintrc.json ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── app ├── drag-to-select │ └── page.tsx ├── layout.tsx └── radix-menu-to-dialog │ └── page.tsx ├── components ├── analytics │ └── fathom.tsx ├── disclosure │ ├── animated.tsx │ ├── animatedWithGoodTransitions.tsx │ ├── animatedWithKeyframes.tsx │ ├── animatedWithLayout.tsx │ ├── animatedWithLayoutWithoutGlitch.tsx │ ├── animatedWithStagger.tsx │ ├── article │ │ ├── defaultValues.tsx │ │ ├── final.tsx │ │ ├── step1.tsx │ │ ├── step2.tsx │ │ └── svgs.tsx │ ├── icons.tsx │ ├── index.ts │ └── noAnimation.tsx ├── drag-to-select │ ├── article │ │ ├── step0-basic-component.tsx │ │ ├── step1-drag-rectangle.tsx │ │ ├── step1.5-drag-rectangle-with-vector.tsx │ │ ├── step2-updating-selection.tsx │ │ ├── step3-preventing-text-selection.tsx │ │ ├── step4-deselection.tsx │ │ ├── step5-scrolling.tsx │ │ ├── step5.5-scrolling-with-clamp.tsx │ │ └── step6-auto-scrolling.tsx │ ├── drag-to-select.tsx │ └── index.ts ├── radix-menu-to-dialog │ ├── components │ │ ├── context-content.tsx │ │ ├── dropdown-content-with-sub-content.tsx │ │ ├── dropdown-content.tsx │ │ ├── dropdown-trigger.tsx │ │ ├── popover-close.tsx │ │ ├── popover-content.tsx │ │ └── subdropdown-content.tsx │ ├── index.ts │ ├── radix-menu-to-dialog.tsx │ └── radix-menu-to-submenu.tsx ├── roving-tabindex │ ├── article │ │ ├── step0 - concept-intro.tsx │ │ ├── step1 - explicit-order.tsx │ │ ├── step2 - dom-order.tsx │ │ ├── step3 - cleaner-component-breakdown.tsx │ │ ├── step4 - tab.tsx │ │ ├── step5 - shift-tab.tsx │ │ ├── step6 - abstraction.tsx │ │ ├── step7 - valueId.tsx │ │ └── step8 - selectors.tsx │ ├── index.ts │ └── roving-tabindex.tsx ├── sidebar │ ├── content.tsx │ ├── gitlab-demo.tsx │ ├── initial-demo.tsx │ ├── linear-demo.tsx │ ├── notion-demo.tsx │ └── perf-slayer.tsx ├── slider │ ├── index.ts │ ├── shapes.tsx │ └── slider.tsx ├── toggle-group │ ├── article │ │ ├── step1 - mouse interaction.tsx │ │ ├── step2 - keyboard shortcuts.tsx │ │ └── step3 - roving tabindex.tsx │ ├── index.tsx │ └── toggle-group.tsx ├── treeview │ ├── article │ │ ├── part-1 │ │ │ ├── step1 - structure.tsx │ │ │ ├── step2 - open.tsx │ │ │ ├── step3 - open-indicator.tsx │ │ │ └── step4 - selection.tsx │ │ ├── part-2 │ │ │ ├── roving-tabindex.tsx │ │ │ ├── step1 - adding-roving-tabindex.tsx │ │ │ ├── step10 - use visibility.mdx │ │ │ ├── step10 - use visibility.tsx │ │ │ ├── step2 - up-down.tsx │ │ │ ├── step3 - right-left.tsx │ │ │ ├── step4 - style.tsx │ │ │ ├── step5 - home-end.tsx │ │ │ ├── step6 - typeahead.tsx │ │ │ ├── step7 - selection.tsx │ │ │ ├── step8 - aria.tsx │ │ │ ├── step9 - voiceover optimization.tsx │ │ │ └── unknown.tsx │ │ └── part-3 │ │ │ ├── animatedTreeview.tsx │ │ │ └── roving-tabindex.tsx │ ├── examples │ │ ├── apple-sidebar.tsx │ │ ├── index.ts │ │ └── initialValue.tsx │ ├── icons.tsx │ ├── index.tsx │ ├── root.tsx │ └── treeview.tsx └── vercel-tabs │ ├── css │ ├── css.tsx │ ├── index.ts │ └── useTabs.tsx │ ├── framer-layout │ ├── framer-layout.tsx │ ├── index.ts │ └── useTabs.tsx │ ├── framer │ ├── framer.tsx │ ├── index.ts │ └── useTabs.tsx │ ├── react-spring │ ├── index.ts │ ├── spring.tsx │ └── useTabs.tsx │ ├── shapes.js │ └── transition-group │ ├── index.ts │ ├── transition-group.tsx │ └── useTabs.tsx ├── lib ├── disclosure │ └── types.ts ├── treeview │ ├── index.ts │ ├── initialValue.tsx │ ├── tree-state.tsx │ └── useTreeNode.tsx └── utils │ ├── chainable-map.ts │ └── index.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ └── hello.ts ├── disclosure.tsx ├── index.tsx ├── roving-tabindex.tsx ├── sidebar │ ├── CSS-vs-framer-motion-vs-motion-one.tsx │ ├── gitlab.tsx │ ├── index.tsx │ ├── initial.tsx │ ├── linear.tsx │ ├── notion.tsx │ └── stair-stepping.tsx ├── slider.tsx ├── toggle-group.tsx ├── treeview.tsx └── vercel-tabs.tsx ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── folder.png ├── next.svg ├── seo.png ├── thirteen.svg └── vercel.svg ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 4, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'all', 10 | bracketSpacing: true, 11 | jsxBracketSameLine: false, 12 | arrowParens: 'avoid', 13 | requirePragma: false, 14 | insertPragma: false, 15 | proseWrap: 'preserve', 16 | htmlWhitespaceSensitivity: 'css', 17 | endOfLine: 'lf', 18 | importOrder: ['', '^(components|templates|lib)/(.*)$', '^[./,../]'], 19 | importOrderSeparation: true, 20 | importOrderCaseInsensitive: true, 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Joshua Wootonn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![React components from scratch](public/seo.png) 2 | 3 | # React components from scratch 4 | 5 | Components are the building blocks of everything we create on the web. In my experience, it has always felt like adding animation and ensuring accessibility were two opposing forces. I made this series to see how many components I could make that are both sexy ✨ and equitable ♿. -------------------------------------------------------------------------------- /app/drag-to-select/page.tsx: -------------------------------------------------------------------------------- 1 | import * as DragToSelect from 'components/drag-to-select' 2 | import Link from 'next/link' 3 | import { Step0Demo } from 'components/drag-to-select/article/step0-basic-component' 4 | import { Step1Demo } from 'components/drag-to-select/article/step1-drag-rectangle' 5 | import { Step15Demo } from 'components/drag-to-select/article/step1.5-drag-rectangle-with-vector' 6 | import { Step2Demo } from 'components/drag-to-select/article/step2-updating-selection' 7 | import { Step3Demo } from 'components/drag-to-select/article/step3-preventing-text-selection' 8 | import { Step4Demo } from 'components/drag-to-select/article/step4-deselection' 9 | import { Step5Demo } from 'components/drag-to-select/article/step5-scrolling' 10 | import { Step55Demo } from 'components/drag-to-select/article/step5.5-scrolling-with-clamp' 11 | import { Step6Demo } from 'components/drag-to-select/article/step6-auto-scrolling' 12 | 13 | const items = new Array(1001).fill(null).map((_, i) => `${i}`) 14 | 15 | export default function Page() { 16 | return ( 17 |
18 |
19 |

Drag to Select

20 |
21 | 22 | 28 | 33 | 34 | 35 | 36 | 44 | 49 | 50 | 51 |
52 |
53 |

54 | This page is the demo for an upcoming blog post. Here is the 55 | final version: 56 |

57 | 58 | {items.map(item => ( 59 | 60 | {item} 61 | 62 | ))} 63 | 64 | 65 |

Intro

66 |

67 | Here is the basic demo rendering items with numbers in assorting 68 | order. 69 |

70 | 71 |

Step 1 is drawing a selection rectangle on drag

72 | 73 |

74 | Step 1.5 is drawing a selection rectangle on drag with a vector 75 |

76 | 77 |

78 | Step 2 updating state based on intersection of rectangles and 79 | the selection rectangle 80 |

81 | 82 |

Step 3 Preventing text selection on drag

83 | 84 |

Step 4 Deselection

85 | 86 |

Step 5 Scrolling

87 | 88 |

Step 5.5 Scrolling (w/ Clamping overflow)

89 | 90 |

Step 6 Auto Scrolling

91 | 92 |
93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Josefin_Sans } from 'next/font/google' 2 | import '../styles/globals.css' 3 | import { FathomPageView } from 'components/analytics/fathom' 4 | import { Suspense } from 'react' 5 | 6 | const josie = Josefin_Sans({ 7 | subsets: ['latin'], 8 | variable: '--josie-font', 9 | }) 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/radix-menu-to-dialog/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { RadixMenuToDialog } from 'components/radix-menu-to-dialog' 3 | import { RadixMenuToSubmenu } from 'components/radix-menu-to-dialog' 4 | 5 | const App = () => { 6 | return ( 7 |
8 |
9 |

10 | Recreating Linear's interactive dropdowns with Radix UI 11 |

12 |
13 | 14 | 20 | 25 | 26 | 27 | 28 | 36 | 41 | 42 | 43 |
44 |
45 |

46 | Here is a demo of a menu and dialog composed together for a 47 | "interactive menu" experience. 48 |

49 | 50 |

51 | Interactive sub dropdown menus don't work in this case 52 | because their shortcuts conflict with text selection. 53 |

54 | 55 |

56 | This is a the companion demo to a blog post I have{' '} 57 | 58 | here. 59 | 60 |

61 |
62 | ) 63 | } 64 | 65 | export default App 66 | -------------------------------------------------------------------------------- /components/analytics/fathom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { usePathname, useSearchParams } from 'next/navigation' 4 | import * as Fathom from 'fathom-client' 5 | import { useEffect } from 'react' 6 | 7 | if (typeof window !== 'undefined') { 8 | Fathom.load(process.env.NEXT_PUBLIC_FATHOM_ID ?? '', { 9 | includedDomains: ['react-components-from-scratch.vercel.app'], 10 | }) 11 | } 12 | 13 | export function FathomPageView() { 14 | const pathname = usePathname() 15 | const searchParams = useSearchParams() 16 | 17 | useEffect(() => { 18 | Fathom.trackPageview() 19 | }, [pathname, searchParams]) 20 | 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /components/disclosure/animated.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | import { content, Faq } from "lib/disclosure/types"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | 68 | ); 69 | }; 70 | 71 | export const Animated = () => { 72 | return ( 73 |
74 | {content.map((c, i) => ( 75 | 76 | ))} 77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /components/disclosure/animatedWithGoodTransitions.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | import { content, Faq } from "lib/disclosure/types"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | 85 | ); 86 | }; 87 | 88 | export const AnimateWithGoodTransition = () => { 89 | return ( 90 |
91 | {content.map((c, i) => ( 92 | 93 | ))} 94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /components/disclosure/animatedWithKeyframes.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "framer-motion"; 2 | import { Faq, content } from "lib/disclosure/types"; 3 | import { useState } from "react"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | 73 | ); 74 | }; 75 | 76 | export const AnimatedWithKeyframes = () => { 77 | return ( 78 |
79 | {content.map((c, i) => ( 80 | 81 | ))} 82 |
83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /components/disclosure/animatedWithLayout.tsx: -------------------------------------------------------------------------------- 1 | import { motion, AnimatePresence, LayoutGroup } from "framer-motion"; 2 | import { Faq, content } from "lib/disclosure/types"; 3 | import { useState } from "react"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | setIsOpen((prev) => !prev)} 13 | layout 14 | > 15 | 16 | 17 | {props.title} 18 | 19 | 20 | 45 | {isOpen ? : } 46 | 47 | 48 | 49 | {isOpen && ( 50 | 63 | {props.description} 64 | 65 | )} 66 | 67 | ); 68 | }; 69 | 70 | export const AnimatedWithLayout = () => { 71 | return ( 72 | 73 |
74 | {content.map((c, i) => ( 75 | 76 | ))} 77 |
78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /components/disclosure/animatedWithLayoutWithoutGlitch.tsx: -------------------------------------------------------------------------------- 1 | import { motion, AnimatePresence, LayoutGroup } from "framer-motion"; 2 | import { Faq, content } from "lib/disclosure/types"; 3 | import { useState } from "react"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | setIsOpen((prev) => !prev)} 16 | layout 17 | > 18 | 19 | 20 | {props.title} 21 | 22 | 23 | 48 | {isOpen ? : } 49 | 50 | 51 | 52 | {isOpen && ( 53 | 66 | {props.description} 67 | 68 | )} 69 | 70 | ); 71 | }; 72 | 73 | export const AnimatedWithLayoutWithoutGlitch = () => { 74 | return ( 75 | 76 |
77 | {content.map((c, i) => ( 78 | 79 | ))} 80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /components/disclosure/animatedWithStagger.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "framer-motion"; 2 | import { Faq, content } from "lib/disclosure/types"; 3 | import { useState } from "react"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | 90 | ); 91 | }; 92 | 93 | export const AnimatedWithStagger = () => { 94 | return ( 95 |
96 | {content.map((c, i) => ( 97 | 98 | ))} 99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /components/disclosure/article/defaultValues.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export type FAQ = { question: string; answer: ReactNode }; 4 | 5 | export const defaultFAQs: FAQ[] = [ 6 | { 7 | question: "Where do you learn to make ice cream?", 8 | answer: ( 9 |
10 | Sundae School 11 |
12 |
13 | Extra text lorem ipsum dolor sit amet, consectetur adipiscing elit. 14 |
15 | ), 16 | }, 17 | { 18 | question: "What’s Forrest Gump’s password?", 19 | answer: ( 20 |
21 | 1forrest1 22 |
23 |
24 | Extra text lorem ipsum dolor sit amet, consectetur adipiscing elit. 25 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum 26 | dolor sit amet, consectetur adipiscing elit. 27 |
28 | ), 29 | }, 30 | { 31 | question: "How do you make holy water?", 32 | answer: ( 33 |
34 | You boil the hell out of it. 35 |
36 |
37 | Extra text lorem ipsum dolor sit amet, consectetur adipiscing elit. 38 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum 39 | dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, 40 | consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur 41 | adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing 42 | elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 43 |
44 | ), 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /components/disclosure/article/final.tsx: -------------------------------------------------------------------------------- 1 | import { motion, AnimatePresence } from "framer-motion"; 2 | import { useState, ReactNode } from "react"; 3 | 4 | import { defaultFAQs } from "./defaultValues"; 5 | import { More, Less } from "./svgs"; 6 | 7 | type Props = { 8 | title: string; 9 | body: ReactNode; 10 | }; 11 | 12 | const Disclosure = (props: Props) => { 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | return ( 16 |
setIsOpen((prev) => !prev)} 19 | > 20 | 53 | 90 | {props.body} 91 | 92 |
93 | ); 94 | }; 95 | 96 | export default function App() { 97 | return ( 98 |
99 | {defaultFAQs.map((faq, i) => ( 100 | 101 | ))} 102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /components/disclosure/article/step1.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "clsx"; 2 | import { useState, ReactNode } from "react"; 3 | 4 | import { defaultFAQs } from "./defaultValues"; 5 | 6 | type Props = { 7 | title: string; 8 | body: ReactNode; 9 | }; 10 | 11 | const Disclosure = (props: Props) => { 12 | const [isOpen, setIsOpen] = useState(false); 13 | 14 | return ( 15 |
setIsOpen((prev) => !prev)} 18 | > 19 | 27 |
31 | {props.body} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default function App() { 38 | return ( 39 |
40 | {defaultFAQs.map((faq, i) => ( 41 | 42 | ))} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/disclosure/article/step2.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "clsx"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | import { useState, ReactNode } from "react"; 4 | 5 | import { defaultFAQs } from "./defaultValues"; 6 | import { Less, More } from "./svgs"; 7 | 8 | type Props = { 9 | title: string; 10 | body: ReactNode; 11 | }; 12 | 13 | const Disclosure = (props: Props) => { 14 | const [isOpen, setIsOpen] = useState(false); 15 | 16 | return ( 17 |
setIsOpen((prev) => !prev)} 20 | > 21 | 54 |
58 | {props.body} 59 |
60 |
61 | ); 62 | }; 63 | 64 | export default function App() { 65 | return ( 66 |
67 | {defaultFAQs.map((faq, i) => ( 68 | 69 | ))} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/disclosure/article/svgs.tsx: -------------------------------------------------------------------------------- 1 | export * from "../icons"; 2 | -------------------------------------------------------------------------------- /components/disclosure/icons.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const More = ({ className, ...props }: SVGProps) => ( 4 | 14 | 22 | 30 | 31 | ); 32 | 33 | export const Less = ({ className, ...props }: SVGProps) => ( 34 | 44 | 52 | 53 | ); 54 | -------------------------------------------------------------------------------- /components/disclosure/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./animated"; 2 | export * from "./animatedWithGoodTransitions"; 3 | export * from "./animatedWithKeyframes"; 4 | export * from "./animatedWithLayout"; 5 | export * from "./animatedWithLayoutWithoutGlitch"; 6 | export * from "./animatedWithStagger"; 7 | export * from "./noAnimation"; 8 | export { default as AnimateWithGoodTransitionAndAccessibility } from "./article/final"; 9 | -------------------------------------------------------------------------------- /components/disclosure/noAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "framer-motion"; 2 | import { Faq, content } from "lib/disclosure/types"; 3 | import { useState } from "react"; 4 | import { Less, More } from "./icons"; 5 | 6 | const Toggle = (props: Faq) => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | return ( 10 | 47 | ); 48 | }; 49 | 50 | export const NoAnimation = () => { 51 | return ( 52 |
53 | {content.map((c, i) => ( 54 | 55 | ))} 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /components/drag-to-select/article/step0-basic-component.tsx: -------------------------------------------------------------------------------- 1 | const items = new Array(30).fill(null).map((_, i) => i + '') 2 | 3 | function Root() { 4 | return ( 5 |
6 |
selectable area
7 |
8 | {items.map(item => ( 9 |
13 | {item} 14 |
15 | ))} 16 |
17 |
18 | ) 19 | } 20 | 21 | export function Step0Demo() { 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /components/drag-to-select/article/step1-drag-rectangle.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from 'react' 3 | 4 | const items = new Array(30).fill(null).map((_, i) => i + '') 5 | 6 | function Root() { 7 | const [selectionRect, setSelectionRect] = useState(null) 8 | 9 | return ( 10 |
11 |
selectable area
12 |
{ 14 | const containerRect = 15 | e.currentTarget.getBoundingClientRect() 16 | 17 | const x = e.clientX - containerRect.x 18 | const y = e.clientY - containerRect.y 19 | 20 | const nextSelectionRect = new DOMRect(x, y, 0, 0) 21 | setSelectionRect(nextSelectionRect) 22 | }} 23 | onPointerMove={e => { 24 | if (selectionRect == null) return 25 | 26 | const containerRect = 27 | e.currentTarget.getBoundingClientRect() 28 | 29 | const x = e.clientX - containerRect.x 30 | const y = e.clientY - containerRect.y 31 | 32 | const nextSelectionRect = new DOMRect( 33 | Math.min(x, selectionRect.x), 34 | Math.min(y, selectionRect.y), 35 | Math.abs(x - selectionRect.x), 36 | Math.abs(y - selectionRect.y), 37 | ) 38 | 39 | setSelectionRect(nextSelectionRect) 40 | }} 41 | onPointerUp={() => { 42 | setSelectionRect(null) 43 | }} 44 | className="relative z-0 grid grid-cols-8 sm:grid-cols-10 gap-4 p-4 border-2 border-black -translate-y-0.5" 45 | > 46 | {items.map(item => ( 47 |
51 | {item} 52 |
53 | ))} 54 | {selectionRect && ( 55 |
64 | )} 65 |
66 |
67 | ) 68 | } 69 | 70 | export function Step1Demo() { 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /components/drag-to-select/article/step1.5-drag-rectangle-with-vector.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from 'react' 3 | 4 | const items = new Array(30).fill(null).map((_, i) => i) 5 | 6 | class DOMVector { 7 | constructor( 8 | readonly x: number, 9 | readonly y: number, 10 | readonly magnitudeX: number, 11 | readonly magnitudeY: number, 12 | ) { 13 | this.x = x 14 | this.y = y 15 | this.magnitudeX = magnitudeX 16 | this.magnitudeY = magnitudeY 17 | } 18 | 19 | toDOMRect(): DOMRect { 20 | return new DOMRect( 21 | Math.min(this.x, this.x + this.magnitudeX), 22 | Math.min(this.y, this.y + this.magnitudeY), 23 | Math.abs(this.magnitudeX), 24 | Math.abs(this.magnitudeY), 25 | ) 26 | } 27 | } 28 | 29 | function Root() { 30 | const [dragVector, setDragVector] = useState(null) 31 | 32 | const selectionRect = dragVector ? dragVector.toDOMRect() : null 33 | 34 | return ( 35 |
36 |
selectable area
37 |
{ 39 | if (e.button !== 0) return 40 | 41 | const containerRect = 42 | e.currentTarget.getBoundingClientRect() 43 | 44 | setDragVector( 45 | new DOMVector( 46 | e.clientX - containerRect.x, 47 | e.clientY - containerRect.y, 48 | 0, 49 | 0, 50 | ), 51 | ) 52 | }} 53 | onPointerMove={e => { 54 | if (dragVector == null) return 55 | 56 | const containerRect = 57 | e.currentTarget.getBoundingClientRect() 58 | 59 | const nextDragVector = new DOMVector( 60 | dragVector.x, 61 | dragVector.y, 62 | e.clientX - containerRect.x - dragVector.x, 63 | e.clientY - containerRect.y - dragVector.y, 64 | ) 65 | 66 | setDragVector(nextDragVector) 67 | }} 68 | onPointerUp={() => { 69 | setDragVector(null) 70 | }} 71 | className="relative z-0 grid grid-cols-8 sm:grid-cols-10 gap-4 p-4 border-2 border-black -translate-y-0.5" 72 | > 73 | {items.map(item => ( 74 |
78 | {item} 79 |
80 | ))} 81 | {selectionRect && ( 82 |
91 | )} 92 |
93 |
94 | ) 95 | } 96 | 97 | export function Step15Demo() { 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /components/drag-to-select/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export { Root, Item } from './drag-to-select' 4 | -------------------------------------------------------------------------------- /components/radix-menu-to-dialog/components/context-content.tsx: -------------------------------------------------------------------------------- 1 | import * as ContextMenu from '@radix-ui/react-context-menu' 2 | import * as Popover from '@radix-ui/react-popover' 3 | import { forwardRef } from 'react' 4 | 5 | const Item = forwardRef(function Item( 6 | props: ContextMenu.ContextMenuItemProps, 7 | ref: React.Ref, 8 | ) { 9 | return ( 10 | 15 | ) 16 | }) 17 | 18 | export function ContextContent(props: ContextMenu.ContextMenuContentProps) { 19 | return ( 20 | 21 | 22 | 23 | Circle 24 | 29 | 30 | 31 | 32 | 33 | Triangle 34 | 39 | 40 | 41 | 42 | 43 | Square 44 | 49 | 50 | 51 | 52 | 53 | Custom 54 | 55 | {/* */} 56 | {/* */} 57 | {/* Custom */} 58 | {/* */} 59 | {/**/} 60 | {/* */} 61 | {/* */} 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /components/radix-menu-to-dialog/components/dropdown-content-with-sub-content.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | import { SubDropdownMenuContent } from "./subdropdown-content"; 3 | 4 | function Item(props: DropdownMenu.DropdownMenuItemProps) { 5 | return ( 6 | 10 | ); 11 | } 12 | 13 | export function DropdownContentInteractiveSubContent( 14 | props: DropdownMenu.DropdownMenuContentProps, 15 | ) { 16 | return ( 17 | 18 | 19 | 20 | Circle 21 | 26 | 27 | 28 | 29 | 30 | Triangle 31 | 36 | 37 | 38 | 39 | 40 | Square 41 | 46 | 47 | 48 | 49 | 50 | 51 | Custom 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/radix-menu-to-dialog/components/dropdown-content.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from '@radix-ui/react-dropdown-menu' 2 | import * as Popover from '@radix-ui/react-popover' 3 | import { forwardRef } from 'react' 4 | 5 | const Item = forwardRef(function Item( 6 | props: DropdownMenu.DropdownMenuItemProps, 7 | ref: React.Ref, 8 | ) { 9 | return ( 10 | 15 | ) 16 | }) 17 | 18 | export function DropdownContent(props: DropdownMenu.DropdownMenuContentProps) { 19 | return ( 20 | 21 | 22 | 23 | Circle 24 | 29 | 30 | 31 | 32 | 33 | Triangle 34 | 39 | 40 | 41 | 42 | 43 | Square 44 | 49 | 50 | 51 | 52 | 53 | Custom 54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /components/radix-menu-to-dialog/components/dropdown-trigger.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Trigger, 3 | DropdownMenuTriggerProps, 4 | } from "@radix-ui/react-dropdown-menu"; 5 | import { ForwardedRef, forwardRef } from "react"; 6 | 7 | export const DropdownMenuTrigger = forwardRef(function DropdownMenuTrigger( 8 | props: DropdownMenuTriggerProps, 9 | ref: ForwardedRef, 10 | ) { 11 | return ( 12 | 13 | 34 | 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /components/radix-menu-to-dialog/components/popover-close.tsx: -------------------------------------------------------------------------------- 1 | import { Close, PopoverCloseProps } from "@radix-ui/react-popover"; 2 | 3 | export function PopoverClose(props: PopoverCloseProps) { 4 | return ( 5 |
6 | 11 | 19 | 24 | 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/radix-menu-to-dialog/components/popover-content.tsx: -------------------------------------------------------------------------------- 1 | import * as Popover from '@radix-ui/react-popover' 2 | import { ComponentProps } from 'react' 3 | import { PopoverClose } from './popover-close' 4 | 5 | function Label(props: ComponentProps<'label'>) { 6 | return