Repository: sambernhardt/ipad-cursor
Branch: master
Commit: 6ac8a16b2f9c
Files: 24
Total size: 21.9 KB
Directory structure:
gitextract_fk40wx2f/
├── .gitignore
├── LICENSE
├── README.md
├── components/
│ ├── GoogleAnalytics/
│ │ ├── init.js
│ │ └── layout.js
│ ├── Providers.js
│ └── components/
│ ├── Footer.js
│ ├── Header.js
│ ├── Hero.js
│ ├── NavLink.js
│ ├── Text/
│ │ └── index.js
│ ├── TextArea.js
│ └── Toggle.js
├── cursor/
│ ├── Context.js
│ ├── Cursor.js
│ ├── Provider.js
│ ├── WithHover.js
│ └── utils.js
├── package.json
├── pages/
│ ├── _app.js
│ ├── _document.js
│ ├── index.js
│ └── test.js
└── theme.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/node_modules
/.next
next.config.js
.now
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 sambernhardt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
`git clone https://github.com/sambernhardt/ipad-cursor.git`
`npm i`
`npm run start`

# Basic usage
## Add the CursorProvider to a page
```javascript
// app.js
import App from 'next/app';
import CursorProvider from '../cursor/Provider';
export default class MyApp extends App {
render () {
const { Component, pageProps } = this.props;
return (
<CursorProvider>
<Component {...pageProps} />
</CursorProvider>
)
}
}
```
## Then wrap your components with the `WithHover` function
```javascript
// Component.js
import WithHover from '../cursor/WithHover';
const Component = () => <h1>;
export default WithHover(<Component />, 'block');
```
### Caveats:
- To move the contents of the hovered component, the component must have a display type of `inline-block` or `block`. CSS transforms don't work on inline elements.
================================================
FILE: components/GoogleAnalytics/init.js
================================================
import ReactGA from "react-ga"
export const initGA = () => {
ReactGA.initialize(process.env.google_analytics)
}
export const logPageView = () => {
ReactGA.set({ page: window.location.pathname })
ReactGA.pageview(window.location.pathname)
}
================================================
FILE: components/GoogleAnalytics/layout.js
================================================
import React, { Component } from "react"
import { initGA, logPageView } from "./init.js"
export default class Layout extends Component {
componentDidMount () {
if (!window.GA_INITIALIZED) {
initGA()
window.GA_INITIALIZED = true
}
logPageView()
}
render () {
return (
<div>
{this.props.children}
</div>
)
}
}
================================================
FILE: components/Providers.js
================================================
import { Fragment, useState, useEffect } from 'react';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
import { Reset } from 'styled-reset';
import useDarkMode from 'use-dark-mode';
import {light, dark} from '../theme';
import CursorProvider from '../cursor/Provider';
export default ({children}) => {
const [mounted, setMounted] = useState(false);
const {value} = useDarkMode(false, { storageKey: null });
useEffect(() => {
setMounted(true)
}, []);
const body =
<Fragment>
<Reset/>
<ThemeProvider theme={value ? dark : light}>
<CursorProvider>
{children}
<GlobalStyle/>
</CursorProvider>
</ThemeProvider>
</Fragment>;
if (!mounted) {
return <div style={{ visibility: 'hidden' }}>{body}</div>
}
return body;
}
const GlobalStyle = createGlobalStyle`
body {
color: ${({theme}) => theme.colors.body};
background: ${({theme}) => theme.colors.background};
font-family: ${({theme}) => theme.fonts.default};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
font-size: ${({theme}) => theme.fontSizes[3]}px;
margin-bottom: ${({theme}) => theme.space[3]}px;
}
`;
================================================
FILE: components/components/Footer.js
================================================
import styled from 'styled-components';
import WithHover from '../../cursor/WithHover';
import Toggle from './Toggle';
const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Link = WithHover(styled.a`
color: ${({theme}) => theme.colors.blue};
text-decoration: none;
z-index: 99;
padding: 8px 2px;
&:hover {
color: ${({theme}) => theme.colors.body};
}
`, 'block');
const Heading = styled.div`
font-size: 32px;
`;
export default () => {
return (
<Container>
<Toggle/>
<Heading style={{ fontSize: '14px' }}>
Find me on <Link href="https://twitter.com/samuelbernhardt">the tweets</Link> or check out <Link href="https://github.com/sambernhardt/ipad-cursor">the code</Link>.
</Heading>
</Container>
)
}
================================================
FILE: components/components/Header.js
================================================
import styled from 'styled-components';
import NavLink from './NavLink';
const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Title = styled.h2`
font-weight: 600;
`;
const Links = styled.div`
display: flex;
font-weight: 600;
`;
const Header = () => {
return (
<Container>
<Title>iPad Cursor</Title>
<Links>
<NavLink>Button 1</NavLink>
<NavLink>Button 2</NavLink>
<NavLink>Button 3</NavLink>
</Links>
</Container>
)
}
export default Header;
================================================
FILE: components/components/Hero.js
================================================
import styled from 'styled-components';
import TextArea from './TextArea';
const Container = styled.div`
min-height: 70vh;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
margin-bottom: 24px;
`;
const Hero = () => {
return (
<Container>
<TextArea
style={{ fontSize: '64px' }}
defaultValue="Hover over this text to see the cursor change size."
focus
placeholder="Header"
/>
<TextArea
defaultValue="Different sized text will change the cursor size appropriately."
placeholder="Subheader"
/>
<TextArea
style={{ fontSize: '14px' }}
defaultValue="Even teeny tiny text works."
placeholder="Small text"
/>
</Container>
)
}
export default Hero;
================================================
FILE: components/components/NavLink.js
================================================
import { useState, useContext } from 'react';
import styled from 'styled-components';
import WithHover from '../../cursor/WithHover';
const Container = styled.div`
padding: 8px 16px;
position: relative;
`;
const NavLink = (props) => {
return (
<Container {...props} />
)
}
export default WithHover(NavLink, 'block');
================================================
FILE: components/components/Text/index.js
================================================
import styled from 'styled-components';
const Subheader = styled.h2`
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
`;
module.exports = {
Subheader
}
================================================
FILE: components/components/TextArea.js
================================================
import { useState, useEffect, useCallback, useContext } from 'react';
import { transparentize } from 'polished';
import styled from 'styled-components';
import autosize from 'autosize';
import WithHover from '../../cursor/WithHover';
const Container = WithHover(styled.textarea`
font-size: 32px;
width: 100%;
background: transparent;
color: ${({ theme }) => theme.colors.body};
font-family: ${({theme}) => theme.fonts.default};
border: none;
margin-bottom: 24px;
resize: none;
&:focus {
outline: none;
}
&::selection {
background: ${({theme}) => transparentize(.6, theme.colors.highlight)};
}
`, 'text');
const TextArea = (props) => {
const [ myRef, setRef ] = useState();
const ref = useCallback(node => {
autosize(node)
setRef(node)
});
useEffect(() => {
if (myRef && props.focus) {
myRef.focus();
}
},[myRef])
return (
<Container passThroughRef={ref} rows="1" {...props}/>
)
}
export default TextArea;
================================================
FILE: components/components/Toggle.js
================================================
import { useContext } from 'react';
import styled from 'styled-components';
import { transparentize } from 'polished';
import WithHover from '../../cursor/WithHover';
import CursorContext from '../../cursor/Context';
const height = 20;
const Container = WithHover(styled.div`
display: inline-flex;
padding: 4px;
position: relative;
align-items: center;
span {
font-size: 13px;
font-weight: 600;
}
`, 'block');
const Dot = styled.div`
background: white;
box-shadow: 0 0 10px rgba(0,0,0,.2);
height: ${height - 4}px;
width: ${height - 4}px;
border-radius: ${(height - 4) / 2}px;
position: absolute;
left: 2px;
top: 2px;
transition: left .3s cubic-bezier(0.075, 0.82, 0.165, 1);
`;
const ToggleContainer = styled.div`
position: relative;
height: ${height}px;
width: ${height * 2}px;
border-radius: ${height / 2}px;
background: ${({theme}) => transparentize(.7, theme.colors.body)};
margin-right: 16px;
overflow: hidden;
${({showingCursor, theme}) => showingCursor && `
background: ${theme.colors.green};
${Dot} {
left: ${height + 2}px;
}
`}
`;
export default ({}) => {
const {toggleCursor, showingCursor} = useContext(CursorContext);
const handleClick = () => {
toggleCursor();
}
return (
<Container onClick={handleClick}>
<ToggleContainer showingCursor={showingCursor}>
<Dot />
</ToggleContainer>
<span>Show cursor</span>
</Container>
)
};
================================================
FILE: cursor/Context.js
================================================
import { createContext } from 'react';
export default createContext("light");
================================================
FILE: cursor/Cursor.js
================================================
import { useContext, useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { transparentize } from 'polished';
import { gsap } from 'gsap';
import CursorContext from './Context';
import { getRelativePosition } from './utils';
const Debug = styled.div`
background: green;
width: 100vw;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
padding: 8px 16px;
> * {
min-width: 200px;
}
`;
const Cursor = styled.div`
width: 24px;
height: 24px;
position: absolute;
background: ${({theme}) => transparentize(.5, theme.colors.cursor)};
border-radius: 12px;
z-index: -1;
transition: opacity .3s;
&.block {
border-radius: 4px;
}
&.text {
height: 30px;
width: 3px;
border-radius: 1px;
}
&.pressing {
opacity: .5;
transition: opacity 0s;
}
`;
const CursorContainer = ({ debug }) => {
const {
pos,
selectedElement,
status,
pressing,
setStatus
} = useContext(CursorContext);
const cursorRef = useRef();
let baseStyles = {
left: pos.x - 12,
top: pos.y - 12,
width: '24px',
height: '24px',
};
// when the selectedElement or status changes
useEffect(() => {
if (!selectedElement.el) return;
if (status == "entering" || status == "shifting") {
// console.log(selectedElement)
if (selectedElement.type == "block") {
gsap.to(cursorRef.current, {
duration: .5,
ease: "elastic.out(1, 1)",
left: selectedElement.el.offsetLeft,
top: selectedElement.el.offsetTop,
height: selectedElement.el.offsetHeight + "px",
width: selectedElement.el.offsetWidth + "px",
borderRadius: '4px',
onComplete: () => {
setStatus("entered");
}
});
}
} else if (status == "exiting") {
// kill all current animations for the block and clear the props it has added
gsap.killTweensOf(cursorRef.current);
}
}, [selectedElement, status]);
useEffect(() => {
// general exit handling
if (status == "exiting" && !selectedElement.el) {
gsap.killTweensOf(cursorRef.current);
gsap.to(cursorRef.current, {
duration: .5,
ease: "elastic.out(1, .5)",
width: '24px',
height: '24px',
x: 0,
y: 0,
left: pos.x - 12,
top: pos.y - 12,
borderRadius: '12px',
onComplete: () => {
setStatus("");
},
});
} else if ((status == "entering" || status == "shifting") && selectedElement.type == "text") {
// text cursor handling
const { textSize } = selectedElement.config;
gsap.killTweensOf(cursorRef.current);
gsap.to(cursorRef.current, {
duration: .5,
ease: "elastic.out(1, 1)",
height: textSize,
width: "3px",
x: 12,
y: (textSize / -2) + 10,
borderRadius: '1px',
onComplete: () => {
setStatus("entered");
}
});
}
}, [pos]);
if (selectedElement.el) {
const amount = 5;
const relativePos = getRelativePosition(pos, selectedElement.el);
const xMid = selectedElement.el.clientWidth / 2;
const yMid = selectedElement.el.clientHeight / 2;
const xMove = (relativePos.x - xMid) / selectedElement.el.clientWidth * amount;
const yMove = (relativePos.y - yMid) / selectedElement.el.clientHeight * amount;
if (selectedElement.type == "block") {
baseStyles = {
left: selectedElement.el.offsetLeft + xMove,
top: selectedElement.el.offsetTop + yMove,
height: selectedElement.el.offsetHeight + "px",
width: selectedElement.el.offsetWidth + "px",
}
}
}
let shapeClass = selectedElement.el && !(status == "entering" || status == "shifting") && selectedElement.type;
return (
<div>
{debug && <Debug>
<span>{JSON.stringify({pos})}</span>
<span>{JSON.stringify(selectedElement.type)}</span>
<span>{JSON.stringify({status})}</span>
</Debug>}
<Cursor
ref={cursorRef}
style={baseStyles}
className={[shapeClass, pressing && "pressing"]}
/>
</div>
)
}
export default CursorContainer;
================================================
FILE: cursor/Provider.js
================================================
import { useState } from 'react';
import { createGlobalStyle } from 'styled-components';
import Cursor from './Cursor';
import Context from './Context';
const GlobalStyle = createGlobalStyle`
body, input, textarea, a {
${({ showingCursor }) => !showingCursor && `
cursor: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjbQg61aAAAADUlEQVQYV2P4//8/IwAI/QL/+TZZdwAAAABJRU5ErkJggg=='),
url(cursor.png),
none;
`}
}
`;
const Provider = ({debug, children}) => {
const [ mousePos, setMousePos ] = useState({ x: 0, y: 0 });
const [ selectedElement, selectedElementSet ] = useState({ el: null });
const [ status, setStatus ] = useState("");
const [ pressing, setPressing ] = useState(false);
const [ showingCursor, showingCursorSet ] = useState(false);
const handleMouseMove = ({ pageX, pageY }) => {
setMousePos({x: pageX, y: pageY})
};
const context = {
pos: mousePos,
selectedElementSet: (element) => {
selectedElementSet(element)
if (!selectedElement.el) {
setStatus("entering")
} else {
setStatus("shifting")
}
},
removeSelectedElement: () => {
setStatus("exiting")
selectedElementSet({ el: null })
},
setStatus: setStatus,
status: status,
selectedElement,
pressing,
toggleCursor: () => showingCursorSet(!showingCursor),
showingCursor: showingCursor
};
return (
<div
onMouseMove={handleMouseMove}
onMouseDown={() => setPressing(true)}
onMouseUp={() => setPressing(false)}
>
<Context.Provider value={context}>
<GlobalStyle showingCursor={showingCursor} />
<Cursor debug={debug} />
{children}
</Context.Provider>
</div>
)
};
export default Provider;
================================================
FILE: cursor/WithHover.js
================================================
import { useState, useContext } from 'react';
import CursorContext from './Context';
import { getRelativePosition } from './utils';
export default (Component, type, config) => ({passThroughRef, ...props}) => {
const context = useContext(CursorContext);
const { selectedElement, pos } = context;
const [ hovering, setHovering ] = useState(false);
const handleMouseEnter = e => {
if (!context.selectedElementSet) return;
let result = {
el: e.currentTarget,
type,
config: {...config}
}
if (type == "text") {
let computed = window.getComputedStyle(e.currentTarget).fontSize;
result.config.textSize = parseFloat(computed.replace("px"));
}
context.selectedElementSet(result);
setHovering(true);
}
const handleMouseLeave = ({pageX, pageY, ...e}) => {
if (!context.removeSelectedElement) return;
context.removeSelectedElement()
setHovering(false);
}
let styles;
if (hovering && selectedElement.el && selectedElement.type == "block") {
const amount = selectedElement.config.hoverOffset ? selectedElement.config.hoverOffset : 2;
const relativePos = getRelativePosition(pos, selectedElement.el);
const xMid = selectedElement.el.offsetWidth / 2;
const yMid = selectedElement.el.offsetHeight / 2;
const xMove = (relativePos.x - xMid) / selectedElement.el.offsetHeight * amount;
const yMove = (relativePos.y - yMid) / selectedElement.el.offsetHeight * amount;
styles = {
transform: `translate(${xMove}px, ${yMove}px)`,
}
}
return <Component
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={styles}
ref={passThroughRef}
{...props}
/>
}
================================================
FILE: cursor/utils.js
================================================
const getRelativePosition = (pageCoords, element) => {
return {
x: pageCoords.x - element.offsetLeft,
y: pageCoords.y - element.offsetTop
}
}
module.exports = {
getRelativePosition
}
================================================
FILE: package.json
================================================
{
"name": "ipad-cursor",
"version": "0.0.1",
"description": "A web implementation of the new iPadOS cursor in Next.js",
"main": "index.js",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"keywords": [
"next.js",
"next",
"styled-components",
"theme"
],
"author": "sdbernhardt@gmail.com",
"license": "ISC",
"dependencies": {
"autosize": "^4.0.2",
"gsap": "^3.2.6",
"isomorphic-unfetch": "^3.0.0",
"next": "^9.3.2",
"polished": "^3.6.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-ga": "^2.7.0",
"styled-components": "^4.3.2",
"styled-reset": "^4.0.0",
"use-dark-mode": "^2.3.1"
},
"devDependencies": {
"babel-plugin-styled-components": "^1.10.6"
}
}
================================================
FILE: pages/_app.js
================================================
import React from 'react';
import App from 'next/app';
import Providers from '../components/Providers.js';
export default class MyApp extends App {
render () {
const { Component, pageProps } = this.props;
return (
<Providers>
<Component {...pageProps} />
</Providers>
)
}
}
================================================
FILE: pages/_document.js
================================================
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps (ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(<App {...props}/>)
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
}
} finally {
sheet.seal()
}
}
}
================================================
FILE: pages/index.js
================================================
import styled from 'styled-components';
import Header from '../components/components/Header';
import Hero from '../components/components/Hero';
import Footer from '../components/components/Footer';
import GoogleAnalytics from "../components/GoogleAnalytics/layout.js"
const Main = styled.div`
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 48px 24px;
box-sizing: border-box;
`;
const Home = () => {
const body = (
<Main>
<Header />
<Hero />
<Footer/>
</Main>
);
return process.env.google_analytics ? <GoogleAnalytics>{body}</GoogleAnalytics> : body
};
export default Home;
================================================
FILE: pages/test.js
================================================
import styled from 'styled-components';
import WithHover from '../cursor/WithHover';
const Link = WithHover(styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 350px;
height: 350px;
border-radius: 8px;
font-size: 24px;
transition-duration: .2s;
&:hover {
transition-duration: 0s;
}
`, 'block', {
hoverOffset: 20
});
const Main = styled.div`
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 48px 24px;
box-sizing: border-box;
`;
export default () => (
<Main>
<Link>Test</Link>
</Main>
)
================================================
FILE: theme.js
================================================
const light = {
colors: {
background: '#fff',
foreground: '#eee',
cursor: '#888',
body: '#222',
black: '#222',
purple: '#11144C',
red: '#E16262',
green: 'rgb(52,199,89)',
yellow: '#FABC60',
blue: 'rgb(0,122,255)',
highlight: '#FABC60',
}
}
const dark = {
colors: {
body: '#fff',
background: 'black',
cursor: '#bbb',
foreground: '#222',
purple: '#11144C',
red: '#E16262',
green: 'rgb(48,209,88)',
yellow: '#FABC60',
blue: 'rgb(10,132,255)',
highlight: 'rgb(10,132,255)',
}
}
const common = {
breakpoints: ['40em', '52em', '64em'],
fonts: {
default: '-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;'
},
fontSizes: [16, 20, 24, 32, 48, 64],
space: [
0, 4, 8, 16, 32, 64, 128, 256
]
}
module.exports = {
light: {...common, ...light},
dark: {...common, ...dark}
}
gitextract_fk40wx2f/ ├── .gitignore ├── LICENSE ├── README.md ├── components/ │ ├── GoogleAnalytics/ │ │ ├── init.js │ │ └── layout.js │ ├── Providers.js │ └── components/ │ ├── Footer.js │ ├── Header.js │ ├── Hero.js │ ├── NavLink.js │ ├── Text/ │ │ └── index.js │ ├── TextArea.js │ └── Toggle.js ├── cursor/ │ ├── Context.js │ ├── Cursor.js │ ├── Provider.js │ ├── WithHover.js │ └── utils.js ├── package.json ├── pages/ │ ├── _app.js │ ├── _document.js │ ├── index.js │ └── test.js └── theme.js
SYMBOL INDEX (7 symbols across 3 files)
FILE: components/GoogleAnalytics/layout.js
class Layout (line 5) | class Layout extends Component {
method componentDidMount (line 6) | componentDidMount () {
method render (line 14) | render () {
FILE: pages/_app.js
class MyApp (line 6) | class MyApp extends App {
method render (line 7) | render () {
FILE: pages/_document.js
class MyDocument (line 4) | class MyDocument extends Document {
method getInitialProps (line 5) | static async getInitialProps (ctx) {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (25K chars).
[
{
"path": ".gitignore",
"chars": 40,
"preview": "/node_modules\n/.next\nnext.config.js\n.now"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2020 sambernhardt\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 952,
"preview": "`git clone https://github.com/sambernhardt/ipad-cursor.git`\n\n`npm i`\n\n`npm run start`\n\n => {\n ReactGA.initialize(process.env.google_analytics)\n}\n \nex"
},
{
"path": "components/GoogleAnalytics/layout.js",
"chars": 373,
"preview": "import React, { Component } from \"react\"\n \nimport { initGA, logPageView } from \"./init.js\"\n \nexport default class Layout"
},
{
"path": "components/Providers.js",
"chars": 1296,
"preview": "import { Fragment, useState, useEffect } from 'react';\nimport { createGlobalStyle, ThemeProvider } from 'styled-componen"
},
{
"path": "components/components/Footer.js",
"chars": 825,
"preview": "import styled from 'styled-components';\nimport WithHover from '../../cursor/WithHover';\nimport Toggle from './Toggle';\n\n"
},
{
"path": "components/components/Header.js",
"chars": 623,
"preview": "import styled from 'styled-components';\n\nimport NavLink from './NavLink';\n\nconst Container = styled.div`\n display: flex"
},
{
"path": "components/components/Hero.js",
"chars": 933,
"preview": "import styled from 'styled-components';\n\nimport TextArea from './TextArea';\n\nconst Container = styled.div`\n min-height:"
},
{
"path": "components/components/NavLink.js",
"chars": 348,
"preview": "import { useState, useContext } from 'react';\nimport styled from 'styled-components';\n\nimport WithHover from '../../curs"
},
{
"path": "components/components/Text/index.js",
"chars": 176,
"preview": "import styled from 'styled-components';\n\nconst Subheader = styled.h2`\n font-size: 16px;\n font-weight: 600;\n mar"
},
{
"path": "components/components/TextArea.js",
"chars": 1018,
"preview": "import { useState, useEffect, useCallback, useContext } from 'react';\nimport { transparentize } from 'polished';\nimport "
},
{
"path": "components/components/Toggle.js",
"chars": 1469,
"preview": "import { useContext } from 'react';\nimport styled from 'styled-components';\nimport { transparentize } from 'polished';\ni"
},
{
"path": "cursor/Context.js",
"chars": 77,
"preview": "import { createContext } from 'react';\nexport default createContext(\"light\");"
},
{
"path": "cursor/Cursor.js",
"chars": 4994,
"preview": "import { useContext, useState, useRef, useEffect } from 'react';\nimport styled from 'styled-components';\nimport { transp"
},
{
"path": "cursor/Provider.js",
"chars": 1924,
"preview": "import { useState } from 'react';\nimport { createGlobalStyle } from 'styled-components';\nimport Cursor from './Cursor';\n"
},
{
"path": "cursor/WithHover.js",
"chars": 1860,
"preview": "import { useState, useContext } from 'react';\nimport CursorContext from './Context';\n\nimport { getRelativePosition } fro"
},
{
"path": "cursor/utils.js",
"chars": 211,
"preview": "const getRelativePosition = (pageCoords, element) => {\n return {\n x: pageCoords.x - element.offsetLeft,\n "
},
{
"path": "package.json",
"chars": 789,
"preview": "{\n \"name\": \"ipad-cursor\",\n \"version\": \"0.0.1\",\n \"description\": \"A web implementation of the new iPadOS cursor in Next"
},
{
"path": "pages/_app.js",
"chars": 311,
"preview": "import React from 'react';\nimport App from 'next/app';\n\nimport Providers from '../components/Providers.js';\n\nexport defa"
},
{
"path": "pages/_document.js",
"chars": 714,
"preview": "import Document from 'next/document'\nimport { ServerStyleSheet } from 'styled-components'\n\nexport default class MyDocume"
},
{
"path": "pages/index.js",
"chars": 634,
"preview": "import styled from 'styled-components';\n\nimport Header from '../components/components/Header';\nimport Hero from '../comp"
},
{
"path": "pages/test.js",
"chars": 603,
"preview": "import styled from 'styled-components';\n\nimport WithHover from '../cursor/WithHover';\n\nconst Link = WithHover(styled.div"
},
{
"path": "theme.js",
"chars": 951,
"preview": "const light = {\n colors: {\n background: '#fff',\n foreground: '#eee',\n cursor: '#888',\n body: '#222',\n bl"
}
]
About this extraction
This page contains the full source code of the sambernhardt/ipad-cursor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (21.9 KB), approximately 6.2k tokens, and a symbol index with 7 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.