├── .build.mjs
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── docs
└── README.zh.md
├── examples
├── nuxt-basic
│ ├── .eslintrc
│ ├── .gitignore
│ ├── .npmrc
│ ├── .stackblitzrc
│ ├── LICENSE
│ ├── README.md
│ ├── app.vue
│ ├── components
│ │ ├── Counter.vue
│ │ ├── DarkToggle.vue
│ │ ├── Footer.vue
│ │ ├── InputEntry.vue
│ │ ├── Logos.vue
│ │ ├── PageView.vue
│ │ └── ToggleEle.vue
│ ├── composables
│ │ ├── count.ts
│ │ ├── cursor.ts
│ │ └── user.ts
│ ├── config
│ │ └── pwa.ts
│ ├── constants
│ │ └── index.ts
│ ├── layouts
│ │ ├── README.md
│ │ ├── default.vue
│ │ └── home.vue
│ ├── netlify.toml
│ ├── nuxt.config.ts
│ ├── package.json
│ ├── pages
│ │ ├── [...all].vue
│ │ ├── hi
│ │ │ └── [id].vue
│ │ └── index.vue
│ ├── plugins
│ │ └── cursor-directive.ts
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── apple-touch-icon.png
│ │ ├── favicon.ico
│ │ ├── maskable-icon.png
│ │ ├── nuxt.svg
│ │ ├── pwa-192x192.png
│ │ ├── pwa-512x512.png
│ │ ├── robots.txt
│ │ └── vite.png
│ ├── server
│ │ ├── api
│ │ │ └── pageview.ts
│ │ └── tsconfig.json
│ ├── tsconfig.json
│ └── uno.config.ts
└── react-basic
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .npmrc
│ ├── index.html
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ └── vite.svg
│ ├── src
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ │ └── react.svg
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── uno.config.ts
│ └── vite.config.ts
├── package.json
├── playground
├── .vscode
│ └── extensions.json
├── index.html
├── public
│ ├── ipad-cursor-dark.svg
│ ├── ipad-cursor.svg
│ ├── og-image.jpg
│ └── screenshot.gif
├── src
│ ├── App.vue
│ ├── assets
│ │ └── vue.svg
│ ├── codes
│ │ ├── basic.ts
│ │ ├── blur-effect.ts
│ │ ├── custom-anime-speed.ts
│ │ ├── custom-bg.ts
│ │ ├── custom-border.ts
│ │ └── install.ts
│ ├── components
│ │ ├── AppHeader.vue
│ │ ├── CodeBox
│ │ │ └── index.vue
│ │ └── LanguageIcon.vue
│ ├── constants
│ │ └── index.ts
│ ├── main.ts
│ ├── pages
│ │ └── index.vue
│ ├── style.css
│ ├── types
│ │ └── lang.ts
│ ├── utils
│ │ └── clipboard.ts
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── uno.config.ts
└── vite.config.ts
├── pnpm-lock.yaml
├── rollup.config.js
├── src
├── index.d.ts
├── index.ts
├── react
│ ├── README.md
│ ├── index.tsx
│ └── tsconfig.json
└── vue
│ └── index.ts
├── tsconfig.json
└── uno.config.ts
/.build.mjs:
--------------------------------------------------------------------------------
1 | import { dirname, resolve } from "path";
2 | import { fileURLToPath } from "url";
3 | import fs from "fs";
4 | import { readFile, writeFile } from "fs/promises";
5 | import { execa } from "execa";
6 | import { execSync } from "child_process";
7 | import chalk from "chalk";
8 | import prompts from "prompts";
9 | import brotliSize from "brotli-size";
10 | import prettyBytes from "pretty-bytes";
11 |
12 | const info = (m) => console.log(chalk.blue(m));
13 | const error = (m) => console.log(chalk.red(m));
14 | const success = (m) => console.log(chalk.green(m));
15 | const details = (m) => console.log(chalk.pink(m));
16 |
17 | const __filename = fileURLToPath(import.meta.url);
18 | const __dirname = dirname(__filename);
19 | const rootDir = resolve(__dirname);
20 | const isPublishing = process.argv[2] === "--publish";
21 |
22 | async function clean() {
23 | if (!fs.existsSync(`${rootDir}/dist`)) return;
24 | await execa("shx", ["rm", "-rf", `${rootDir}/dist`]);
25 | }
26 |
27 | async function baseBuild() {
28 | info("Rolling up primary package");
29 | await execa("npx", ["rollup", "-c", "rollup.config.js"]);
30 | }
31 |
32 | async function baseBuildMin() {
33 | info("Minifying primary package");
34 | await execa("npx", [
35 | "rollup",
36 | "-c",
37 | "rollup.config.js",
38 | "--environment",
39 | "MIN:true",
40 | ]);
41 | }
42 |
43 | async function vueBuild() {
44 | info("Rolling up Vue package");
45 | await execa("npx", [
46 | "rollup",
47 | "-c",
48 | "rollup.config.js",
49 | "--environment",
50 | "FRAMEWORK:vue",
51 | ]);
52 | /**
53 | * This is a super hack — for some reason these imports need to be explicitly
54 | * to .mjs files so...we make it so.
55 | */
56 | let raw = await readFile(resolve(rootDir, "dist/vue/index.mjs"), "utf8");
57 | raw = raw.replace("from '../index'", "from '../index.mjs'");
58 | await writeFile(resolve(rootDir, "dist/vue/index.mjs"), raw);
59 | }
60 |
61 | async function reactBuild() {
62 | info("Rolling up React package");
63 | await execa("npx", [
64 | "rollup",
65 | "-c",
66 | "rollup.config.js",
67 | "--environment",
68 | "FRAMEWORK:react",
69 | ]);
70 | /**
71 | * This is a super hack — for some reason these imports need to be explicitly
72 | * to .mjs files so...we make it so.
73 | */
74 | let raw = await readFile(resolve(rootDir, "dist/react/index.mjs"), "utf8");
75 | raw = raw.replace("from '../index'", "from '../index.mjs'");
76 | await writeFile(resolve(rootDir, "dist/react/index.mjs"), raw);
77 | }
78 |
79 | async function declarationsBuild() {
80 | info("Outputting declarations");
81 | await execa("npx", [
82 | "rollup",
83 | "-c",
84 | "rollup.config.js",
85 | "--environment",
86 | "DECLARATIONS:true",
87 | ]);
88 | }
89 |
90 | async function bundleDeclarations() {
91 | info("Bundling declarations");
92 | await execa("shx", [
93 | "mv",
94 | `${rootDir}/dist/src/index.d.ts`,
95 | `${rootDir}/dist/index.d.ts`,
96 | ]);
97 | await execa("shx", [
98 | "cp",
99 | `${rootDir}/dist/index.d.ts`,
100 | `${rootDir}/src/index.d.ts`,
101 | ]);
102 | await execa("shx", [
103 | "mv",
104 | `${rootDir}/dist/src/vue/index.d.ts`,
105 | `${rootDir}/dist/vue/index.d.ts`,
106 | ]);
107 | await execa("shx", ["rm", "-rf", `${rootDir}/dist/src`]);
108 | await execa("shx", ["rm", `${rootDir}/dist/index.js`]);
109 | await execa("pnpm", ["build:react-ts"]);
110 | }
111 |
112 | async function addPackageJSON() {
113 | info("Writing package.json");
114 | const raw = await readFile(resolve(rootDir, "package.json"), "utf8");
115 | const packageJSON = JSON.parse(raw);
116 | delete packageJSON.private;
117 | delete packageJSON.devDependencies;
118 | delete packageJSON.scripts;
119 | await writeFile(
120 | resolve(rootDir, "dist/package.json"),
121 | JSON.stringify(packageJSON, null, 2)
122 | );
123 | }
124 |
125 | async function addAssets() {
126 | info("Writing readme and license.");
127 | await execa("shx", [
128 | "cp",
129 | `${rootDir}/README.md`,
130 | `${rootDir}/dist/README.md`,
131 | ]);
132 | await execa("shx", ["cp", `${rootDir}/LICENSE`, `${rootDir}/dist/LICENSE`]);
133 | }
134 |
135 | async function prepareForPublishing() {
136 | info("Preparing for publication");
137 | if (!/npm-cli\.js$/.test(process.env.npm_execpath)) {
138 | error(`⚠️ You must run this command with npm instead of yarn.`);
139 | info("Please try again with:\n\n» npm run publish\n\n");
140 | process.exit();
141 | }
142 | const isClean = !execSync(`git status --untracked-files=no --porcelain`, {
143 | encoding: "utf-8",
144 | });
145 | if (!isClean) {
146 | error("Commit your changes before publishing.");
147 | process.exit();
148 | }
149 | const raw = await readFile(resolve(rootDir, "package.json"), "utf8");
150 | const packageJSON = JSON.parse(raw);
151 | const response = await prompts([
152 | {
153 | type: "confirm",
154 | name: "value",
155 | message: `Confirm you want to publish version ${chalk.red(
156 | packageJSON.version
157 | )}?`,
158 | initial: false,
159 | },
160 | ]);
161 | if (!response.value) {
162 | error("Please adjust the version and try again");
163 | process.exit();
164 | }
165 | }
166 |
167 | async function publish() {
168 | const raw = await readFile(resolve(rootDir, "package.json"), "utf8");
169 | const packageJSON = JSON.parse(raw);
170 | const response = await prompts([
171 | {
172 | type: "confirm",
173 | name: "value",
174 | message: `Project is build. Ready to publish?`,
175 | initial: false,
176 | },
177 | ]);
178 | if (response.value) {
179 | execSync("npm publish ./dist --registry https://registry.npmjs.org");
180 | // use `npx bumpp` to bump version, create and push tag
181 | // execSync(`git tag ${packageJSON.version}`);
182 | // execSync(`git push origin --tags`);
183 | }
184 | }
185 |
186 | async function outputSize() {
187 | const raw = await readFile(resolve(rootDir, "dist/index.min.js"), "utf8");
188 | console.log("Brotli size: " + prettyBytes(brotliSize.sync(raw)));
189 | }
190 |
191 | if (isPublishing) await prepareForPublishing();
192 | await clean();
193 | await baseBuild();
194 | await baseBuildMin();
195 | await vueBuild();
196 | await reactBuild();
197 | await declarationsBuild();
198 | await bundleDeclarations();
199 | await addPackageJSON();
200 | await addAssets();
201 | await outputSize();
202 | isPublishing ? await publish() : success("Build complete");
203 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .DS_Store
3 | node_modules
4 | coverage
5 | temp
6 | *.log
7 | /.aws/credentials
8 | .yalc
9 | .idea
10 | .vercel
11 | .vite-ssg-temp
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Attributify",
4 | "brotli",
5 | "btns",
6 | "darcula",
7 | "execa",
8 | "highlightjs",
9 | "Mergeble",
10 | "mousemove",
11 | "prismjs",
12 | "rgba",
13 | "Roadmap",
14 | "srcset",
15 | "taze",
16 | "unhead"
17 | ]
18 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 CatsJuice
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ipad-curosr
11 |
12 |
13 | Mouse effect hacking of iPad in browser that can be used in any frameworks
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ---
34 |
35 | ## Install
36 |
37 | - NPM
38 |
39 | ```bash
40 | npm install ipad-cursor --save
41 | ```
42 |
43 | - CDN
44 |
45 | Only support `ESM` module
46 |
47 | ```html
48 | Block
49 | text
50 |
51 |
55 | ```
56 |
57 | See [cursor.oooo.so](https://ipad-cursor.oooo.so) for more details.
58 |
59 | ## Usage
60 |
61 | ### Basic usage
62 |
63 | Apply `data-cursor` attribute to the element you want to add the effect.
64 |
65 | - `data-cursor="text"`: text cursor
66 | - `data-cursor="block"`: block cursor
67 |
68 | ```html
69 | Text Cursor
70 | Block Cursor
71 | ```
72 |
73 | After your dom loaded, call `initCursor` to start the effect. You may need to call `updateCursor()` when dom updated.
74 |
75 | ```js
76 | import { initCursor } from 'ipad-cursor'
77 |
78 | initCursor()
79 | ```
80 |
81 | > ⚠️ **Notice**: As so far, you need to manage `when to updateCursor` yourself. Make sure to call `updateCursor` after dom updated.
82 | > In the future, there maybe a better way to handle this, see [Roadmap](#roadmap) for more details.
83 |
84 | ### Custom Style
85 |
86 | You can customize the style of the cursor by [Config](#config), config can be passed to `initCursor` method, or use `updateConfig` method to update config. Every type can be configured separately.
87 |
88 | ```ts
89 | import { initCursor, updateConfig } from 'ipad-cursor'
90 | import type { IpadCursorConfig, IpadCursorStyle } from 'ipad-cursor'
91 |
92 | const normalStyle: IpadCursorStyle = { background: 'red' }
93 | const textStyle: IpadCursorStyle = { background: 'blue' }
94 | const blockStyle: IpadCursorStyle = { background: 'green' }
95 | const config: IpadCursorConfig = {
96 | normalStyle,
97 | textStyle,
98 | blockStyle,
99 | };
100 | initCursor(config)
101 | ```
102 |
103 | Sometimes, you may want to customize the style of the cursor for a specific element, you can use `data-cursor-style` attribute to do this.
104 |
105 | The value of `data-cursor-style` attribute is a string, split by `;`, and each part is a style, split by `:`. For example, `background:red;color:blue`.
106 |
107 | It is recommended to use [customCursorStyle](#customCursorStyle%28style%29) method to create style string.
108 |
109 | For example, customize the style for a circle element (Like avatar).
110 |
111 | ```html
112 |
117 |
118 |
122 | ```
123 |
124 | See [Style](#style) for full style list.
125 |
126 | ### Use in framework
127 |
128 | - [Vue.js](https://vuejs.org/)
129 | - **hooks**
130 |
131 | You can use `useCursor` hook to call `updateCursor()` automatically on mounted and unmounted.
132 | ```ts
133 |
138 | ```
139 | - **directive** (v0.5.2+)
140 |
141 | Register plugin globally
142 | ```ts
143 | // src/main.ts
144 | import { ipadCursorPlugin } from "ipad-cursor/vue"
145 |
146 | app.use(ipadCursorPlugin, {
147 | // global configurations
148 | blockStyle: { radius: "auto" }
149 | })
150 | ```
151 |
152 | Use in component
153 | ```html
154 |
155 |
156 |
157 | ```
158 |
159 | - [React](https://react.dev)
160 | See [App.tsx](./examples/react-basic/src/App.tsx)
161 | - [Hexo](https://hexo.io/)
162 | See [@zqqcee](https://github.com/zqqcee)'s [Blog](https://zqqcee.github.io/2023/07/23/ebae3e5deab8/)
163 |
164 | ## Principle
165 |
166 | When `initCursor` called, it will remove default cursor, and generate a fake cursor use `div` element. Then listen `mousemove` event, and move the fake cursor to the mouse position.
167 |
168 | After init finished, it will call `updateCursor` method, scan element with `data-cursor` attribute, detect the cursor type, and add event listener to the element.
169 |
170 | When mouse enter the element, apply styles.
171 |
172 | ## API
173 |
174 | ### initCursor(cfg)
175 | > see [Config](#config) for more details.
176 |
177 | Init cursor, remove default cursor, and generate a fake cursor use `div` element. Then listen `mousemove` event, and move the fake cursor to the mouse position.
178 |
179 |
180 | ### updateCursor
181 | Scan element to observe hover event, and apply styles, as well as remove unused element's event listener.
182 |
183 | ### disposeCursor
184 | Remove fake cursor, and remove all event listener, recover default cursor.
185 |
186 | ### updateConfig(cfg)
187 | Update config, see [Config](#config) for more details.
188 |
189 | ### customCursorStyle(style)
190 | Create style string that can be used as `data-cursor-style` attribute.
191 | This method is used for better type hinting.
192 |
193 | ### resetCursor
194 | Reset cursor to default style.
195 |
196 | ## Config
197 |
198 | It is recommended to see [index.d.ts](./src/index.d.ts) in the npm package.
199 |
200 | | Name | Type | Default | Description | required |
201 | | ------------------------------------------------- | ----------------- | ------------------- | -------------------------------------------------------------------------------------- | -------- |
202 | | `adsorptionStrength` | `number` | `0.2` | The strength of adsorption effect, number between 0 and 30 | No |
203 | | `className` | `string` | `'ipad-cursor'` | The class name of fake cursor | No |
204 | | `blockPadding` | `number` | `auto` | The padding of cursor when hover on block, set to `auto` will calculate automatic | No |
205 | | `enableAutoTextCursor`(`v0.2.0+`) | `boolean` | `false` | Auto detect text cursor, see [#12](https://github.com/CatsJuice/ipad-cursor/pull/12) | No |
206 | | `enableLighting`(`v0.3.0+`) | `boolean` | `false` | Add a lighting effect to block [#14](https://github.com/CatsJuice/ipad-cursor/pull/14) | No |
207 | | `enableMouseDownEffect`(`v0.4.3+`, Experimental) | `boolean` | `false` | Add a effect when mouse down, customize style by config `mouseDownStyle` | No |
208 | | `enableAutoUpdateCursor`(`v0.5.0+`) | `boolean` | `false` | Auto update cursor when dom updated | No |
209 | | `normalStyle` | `IpadCursorStyle` | see [Style](#style) | The style of normal cursor, see [Style](#style) | No |
210 | | `textStyle` | `IpadCursorStyle` | see [Style](#style) | The style of text cursor, see [Style](#style) | No |
211 | | `blockStyle` | `IpadCursorStyle` | see [Style](#style) | The style of block cursor, see [Style](#style) | No |
212 | | `mouseDownStyle`(`v0.4.3+`, Experimental) | `IpadCursorStyle` | see [Style](#style) | The style of cursor when mouse is pressed, see [Style](#style) | No |
213 |
214 | ## Style
215 |
216 | | Name | Type | Description | example |
217 | | ------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
218 | | `width` | `MaybeSize` | The width of cursor | `'10px'`, `10`, `'10'` |
219 | | `height` | `MaybeSize` | The height of cursor | `'10px'`, `10`, `'10'` |
220 | | `radius` | `MaybeSize` \| `'auto'` | The border radius of cursor, if set to `auto` for `blockStyle`, it will be calculated by dom's css `border-radius` and `config.blockPadding`. | `'10px'`, `10`, `'10'`, `'auto'` |
221 | | `background` | `string` | The background color of cursor | `'#fff'`, `'red'`, `'rgba(0,0,0)'` |
222 | | `border` | `string` | The css border property of cursor | `'1px solid black'` |
223 | | `zIndex` | `number` | The z-index of cursor | `1` |
224 | | `scale` | `number` | The scale of cursor | `1.05` |
225 | | `backdropBlur` | `MaybeSize` | The backdrop-filter blur of cursor | `'10px'`, `10`, `'10'` |
226 | | `backdropSaturate` | `string` | The backdrop-filter saturate of cursor | `180%` |
227 | | `durationBase` | `MaybeDuration` | Transition duration of basic properties like `width`, `height`, `radius`, `border`, `background-color`, if unit if not specified, `ms` will be used | `'1000'`, `1000`, `200ms`, `0.23s` |
228 | | `durationPosition` | `MaybeDuration` | Transition duration of position properties like `top`, `left`, if unit if not specified, `ms` will be used | `'1000'`, `1000`, `200ms`, `0.23s` |
229 | | `durationBackdropFilter` | `MaybeDuration` | Transition duration of backdrop-filter property, if unit if not specified, `ms` will be used | `'1000'`, `1000`, `200ms`, `0.23s` |
230 |
231 | ### Default Style
232 |
233 | See `getDefaultConfig` method in [index.ts](./src/index.ts) for more details.
234 |
235 |
236 | ## Roadmap
237 |
238 | - [x] Add Chinese document
239 | - [x] API Docs
240 | - [ ] More examples
241 | - [x] Auto detect dom update, and call `updateCursor` automatically
242 | - Maybe use [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
243 |
244 |
245 | ## Showcase
246 |
247 | - [oooo.so](https://oooo.so)
248 | - [ipad-cursor.oooo.so](https://ipad-cursor.oooo.so)
249 |
--------------------------------------------------------------------------------
/docs/README.zh.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ipad-mouse
11 |
12 |
13 | 在浏览器中实现 iPad 的鼠标效果,可在任何框架中使用
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ---
35 |
36 |
37 | ## 安装
38 |
39 | - NPM
40 |
41 | ```bash
42 | npm install ipad-cursor --save
43 | ```
44 |
45 | - CDN
46 |
47 | 目前仅支持 `ESM` 模块
48 | ```html
49 | Block
50 | text
51 |
52 |
56 | ```
57 |
58 | 更多详细信息请查看 [cursor.oooo.so](https://ipad-cursor.oooo.so)。
59 |
60 | ## 使用
61 |
62 | ### 基本使用
63 |
64 | 在你想要添加效果的元素上应用 `data-cursor` 属性。
65 |
66 | - `data-cursor="text"`: 文本光标
67 | - `data-cursor="block"`: 块状光标
68 |
69 | ```html
70 | 文本光标
71 | 块状光标
72 | ```
73 |
74 | 在你的dom加载后,调用 `initCursor` 来启动效果。当dom更新时,你可能需要调用 `updateCursor()`。
75 |
76 | ```js
77 | import { initCursor } from 'ipad-cursor'
78 |
79 | initCursor()
80 | ```
81 |
82 | > ⚠️ **注意**:到目前为止,你需要自己管理 `何时更新光标`。确保在dom更新后调用 `updateCursor`。
83 | > 在未来,可能会有更好的方式来处理这个问题,详见 [路线图](#路线图)。
84 |
85 | ### 自定义样式
86 |
87 | 你可以通过配置 [配置](#配置) 来自定义光标的样式,可以在调用 `initCursor` 时传入配置,也可以在初始化后调用 `updateConfig` 来更新配置。每一种光标类型都可以单独定义样式。
88 |
89 | ```ts
90 | import { initCursor, updateConfig } from 'ipad-cursor'
91 | import type { IpadCursorConfig, IpadCursorStyle } from 'ipad-cursor'
92 |
93 | const normalStyle: IpadCursorStyle = { background: 'red' }
94 | const textStyle: IpadCursorStyle = { background: 'blue' }
95 | const blockStyle: IpadCursorStyle = { background: 'green' }
96 | const config: IpadCursorConfig = {
97 | normalStyle,
98 | textStyle,
99 | blockStyle,
100 | };
101 | initCursor(config)
102 | ```
103 |
104 | 有时候,你可能需要针对某个元素自定义样式, 可以通过给元素绑定 `data-cursor-style` 来设置样式,该属性的值是由 `;` 分割的多个 `key:value` 对,其中 `key` 是 `IpadCursorStyle` 的属性,`value` 是对应的值,如果传入的 `key` 不是 `IpadCursorStyle` 的属性,会被当作 `css` 属性。
105 |
106 | 推荐通过 [customCursorStyle](#customCursorStyle%28style%29) 方法来创建样式字符串以获得更好的类型提示。
107 |
108 | 例如,为一个圆形的元素(如头像),需要单独设置:
109 |
110 | ```html
111 |
116 |
117 |
121 | ```
122 |
123 | 查看 [样式](#样式) 浏览完整的样式列表
124 |
125 | ### 在框架中使用
126 |
127 | - [Vue.js](https://vuejs.org/)
128 | - **hooks**
129 |
130 | 你可以通过使用 `useCursor` hook, 当组件挂载和销毁时自动调用 `updateCursor()`
131 | ```ts
132 |
137 | ```
138 | - **自定义指令** (v0.5.2+)
139 |
140 | 全局注册插件:
141 | ```ts
142 | // src/main.ts
143 | import { ipadCursorPlugin } from "ipad-cursor/vue"
144 |
145 | app.use(ipadCursorPlugin, {
146 | // global configurations
147 | blockStyle: { radius: "auto" }
148 | })
149 | ```
150 |
151 | 然后就可以在组件中使用:
152 | ```html
153 |
154 |
155 |
156 | ```
157 |
158 | - [React](https://react.dev)
159 | 可参考 [App.tsx](./examples/react-basic/src/App.tsx)
160 | - [Hexo](https://hexo.io/zh-cn/)
161 | 可参考 [@zqqcee](https://github.com/zqqcee)'s [Blog](https://zqqcee.github.io/2023/07/23/ebae3e5deab8/)
162 |
163 |
164 | ## 原理
165 |
166 | 当调用 `initCursor` 时,它将移除默认光标,并使用 `div` 元素生成一个假光标。然后监听 `mousemove` 事件,并将假光标移动到鼠标位置。
167 |
168 | 初始化完成后,它将调用 `updateCursor` 方法,扫描带有 `data-cursor` 属性的元素,检测光标类型,并为元素添加事件监听器。
169 |
170 | 当鼠标进入元素时,应用样式。
171 |
172 | ## API
173 |
174 | ### initCursor(cfg)
175 | > 更多详细信息请查看 [配置](#配置)。
176 |
177 | 初始化光标,移除默认光标,并使用 `div` 元素生成一个假光标。然后监听 `mousemove` 事件,并将假光标移动到鼠标位置。
178 |
179 | ### updateCursor
180 | 扫描元素以观察悬停事件,并应用样式,以及移除未使用元素的事件监听器。
181 |
182 | ### disposeCursor
183 | 移除假光标,并移除所有事件监听器,恢复默认光标。
184 |
185 | ### updateConfig(cfg)
186 | 更新配置,详见 [配置](#配置)。
187 |
188 | ### customCursorStyle(style)
189 | 创建可用作 `data-cursor-style` 属性的样式字符串。
190 | 这个方法用于更好的类型提示。
191 |
192 | ### resetCursor
193 | 将光标重置为默认样式。
194 |
195 | ## 配置
196 |
197 | 建议查看 npm 包中的 [index.d.ts](./src/index.d.ts)。
198 |
199 | | 名称 | 类型 | 默认值 | 描述 | 是否必须 |
200 | | ------------------------------------------- | ----------------- | -------------------- | ---------------------------------------------------------------------------------- | -------- |
201 | | `adsorptionStrength` | `number` | `0.2` | 吸附力强度 | No |
202 | | `className` | `string` | `'ipad-cursor'` | 光标的css类名 | No |
203 | | `blockPadding` | `number` | `auto` | 当光标聚焦在 `block` 时的内边距,设置为 `auto` 将自动计算 | No |
204 | | `enableAutoTextCursor`(`v0.2.0+`) | `boolean` | `false` | 自动检测 `text` 类型的光标 [#12](https://github.com/CatsJuice/ipad-cursor/pull/12) | No |
205 | | `enableLighting`(`v0.3.0+`) | `boolean` | `false` | 给 `block` 增加光照效果 [#14](https://github.com/CatsJuice/ipad-cursor/pull/14) | No |
206 | | `enableMouseDownEffect`(`v0.4.3+`, 实验性) | `boolean` | `false` | 当鼠标按下时应用样式, 通过配置 `mouseDownStyle` 来自定义样式 | No |
207 | | `enableAutoUpdateCursor`(`v0.5.0+`) | `boolean` | `false` | 当 dom 更新时自动更更新光标的扫描 | No |
208 | | `normalStyle` | `IpadCursorStyle` | 请查看 [样式](#样式) | 正常情况下的光标样式, 请查看 [样式](#样式) | No |
209 | | `textStyle` | `IpadCursorStyle` | 请查看 [样式](#样式) | 文字模式下的光标样式, 请查看 [样式](#样式) | No |
210 | | `blockStyle` | `IpadCursorStyle` | 请查看 [样式](#样式) | 块元素下的光标样式, 请查看 [样式](#样式) | No |
211 | | `mouseDownStyle`(`v0.4.3+`, 实验性) | `IpadCursorStyle` | 请查看 [样式](#样式) | 鼠标按下时的光标样式, 请查看 [样式](#样式) | No |
212 |
213 |
214 | ## 样式
215 |
216 | | 名称 | 类型 | 描述 | 例子 |
217 | | ------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------- |
218 | | `width` | `MaybeSize` | 光标宽度 | `'10px'`, `10`, `'10'` |
219 | | `height` | `MaybeSize` | 光标高度 | `'10px'`, `10`, `'10'` |
220 | | `radius` | `MaybeSize` \| `'auto'` | 光标半径, 当给 `blockStyle` 设置为 `auto` ,将根据元素的 css `border-radius` 和 `config.blockPadding` 自动计算 | `'10px'`, `10`, `'10'`, `'auto'` |
221 | | `background` | `string` | 光标背景颜色 | `'#fff'`, `'red'`, `'rgba(0,0,0)'` |
222 | | `border` | `string` | 光标边框的css样式 | `'1px solid black'` |
223 | | `zIndex` | `number` | 光标的z-index层级 | `1` |
224 | | `scale` | `number` | 光标缩放 | `1.05` |
225 | | `backdropBlur` | `MaybeSize` | 光标的 backdrop-filter 模糊 | `'10px'`, `10`, `'10'` |
226 | | `backdropSaturate` | `string` | 光标的 backdrop-filter 饱和度 | `180%` |
227 | | `durationBase` | `MaybeDuration` | 光标的基础属性过度时间 如 `width`, `height`, `radius`, `border`, `background-color`, 如果未指定单位, 将会使用 `ms` | `'1000'`, `1000`, `200ms`, `0.23s` |
228 | | `durationPosition` | `MaybeDuration` | 光标的位置属性过度时间 如 `top`, `left`, 如果未指定单位, 将会使用 `ms` | `'1000'`, `1000`, `200ms`, `0.23s` |
229 | | `durationBackdropFilter` | `MaybeDuration` | 光标的backdrop-filter属性过度时间, 如果未指定单位, 将会使用 `ms` | `'1000'`, `1000`, `200ms`, `0.23s` |
230 |
231 | ### 默认样式
232 |
233 | 请查看 [index.ts](./src/index.ts) 中的 `getDefaultConfig` 方法
234 |
235 |
236 | ## 路线图
237 |
238 | - [x] 添加中文文档
239 | - [x] API 文档
240 | - [ ] 更多示例
241 | - [x] 自动检测 dom 更新,并自动调用 `updateCursor`
242 | - 可能会使用 [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
243 |
244 |
245 | ## 展示
246 |
247 | - [oooo.so](https://oooo.so)
248 | - [ipad-cursor.oooo.so](https://ipad-cursor.oooo.so)
--------------------------------------------------------------------------------
/examples/nuxt-basic/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@antfu",
4 | "@unocss"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | dist
4 | .output
5 | .nuxt
6 | .env
7 | .idea/
8 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 | shell-emulator=true
4 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/.stackblitzrc:
--------------------------------------------------------------------------------
1 | {
2 | "installDependencies": true,
3 | "startCommand": "npm run dev"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-PRESENT Anthony Fu
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 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vitesse for Nuxt 3
7 |
8 |
9 |
10 | 🧪 Working in Progress
11 |
12 |
13 |
14 |
15 | 🖥 Online Preview
16 |
17 |
18 |
19 |
20 | ## Features
21 |
22 | - 💚 [Nuxt 3](https://nuxt.com/) - SSR, ESR, File-based routing, components auto importing, modules, etc.
23 |
24 | - ⚡️ Vite - Instant HMR.
25 |
26 | - 🎨 [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand atomic CSS engine.
27 |
28 | - 😃 Use icons from any icon sets in Pure CSS, powered by [UnoCSS](https://github.com/unocss/unocss).
29 |
30 | - 🔥 The `
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
35 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/Counter.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ count }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/DarkToggle.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/InputEntry.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
24 |
25 |
31 | GO
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/Logos.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Nuxt 3
6 |
7 |
12 |
13 |
14 |
Vitesse
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/PageView.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {{ data?.pageview }}
10 | page views since
11 | {{ time }}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/components/ToggleEle.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 | toggle
17 |
18 |
19 |
20 | toggle
21 |
22 |
23 |
24 |
25 | toggle
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/composables/count.ts:
--------------------------------------------------------------------------------
1 | export function useCount() {
2 | const count = useState('count', () => Math.round(Math.random() * 20))
3 |
4 | function inc() {
5 | count.value += 1
6 | }
7 | function dec() {
8 | count.value -= 1
9 | }
10 |
11 | return {
12 | count,
13 | inc,
14 | dec,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/composables/cursor.ts:
--------------------------------------------------------------------------------
1 | // production
2 | // import { useCursor } from 'ipad-cursor/vue'
3 |
4 | // development
5 | import { useCursor } from '../../../src/vue'
6 |
7 | export default useCursor
8 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/composables/user.ts:
--------------------------------------------------------------------------------
1 | import { acceptHMRUpdate, defineStore } from 'pinia'
2 |
3 | export const useUserStore = defineStore('user', () => {
4 | /**
5 | * Current named of the user.
6 | */
7 | const savedName = ref('')
8 | const previousNames = ref(new Set())
9 |
10 | const usedNames = computed(() => Array.from(previousNames.value))
11 | const otherNames = computed(() => usedNames.value.filter(name => name !== savedName.value))
12 |
13 | /**
14 | * Changes the current name of the user and saves the one that was used
15 | * before.
16 | *
17 | * @param name - new name to set
18 | */
19 | function setNewName(name: string) {
20 | if (savedName.value)
21 | previousNames.value.add(savedName.value)
22 |
23 | savedName.value = name
24 | }
25 |
26 | return {
27 | setNewName,
28 | otherNames,
29 | savedName,
30 | }
31 | })
32 |
33 | if (import.meta.hot)
34 | import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
35 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/config/pwa.ts:
--------------------------------------------------------------------------------
1 | import type { ModuleOptions } from '@vite-pwa/nuxt'
2 | import { appDescription, appName } from '../constants/index'
3 |
4 | const scope = '/'
5 |
6 | export const pwa: ModuleOptions = {
7 | registerType: 'autoUpdate',
8 | scope,
9 | base: scope,
10 | manifest: {
11 | id: scope,
12 | scope,
13 | name: appName,
14 | short_name: appName,
15 | description: appDescription,
16 | theme_color: '#ffffff',
17 | icons: [
18 | {
19 | src: 'pwa-192x192.png',
20 | sizes: '192x192',
21 | type: 'image/png',
22 | },
23 | {
24 | src: 'pwa-512x512.png',
25 | sizes: '512x512',
26 | type: 'image/png',
27 | },
28 | {
29 | src: 'maskable-icon.png',
30 | sizes: '512x512',
31 | type: 'image/png',
32 | purpose: 'any maskable',
33 | },
34 | ],
35 | },
36 | workbox: {
37 | globPatterns: ['**/*.{js,css,html,txt,png,ico,svg}'],
38 | navigateFallbackDenylist: [/^\/api\//],
39 | navigateFallback: '/',
40 | cleanupOutdatedCaches: true,
41 | runtimeCaching: [
42 | {
43 | urlPattern: /^https:\/\/fonts.googleapis.com\/.*/i,
44 | handler: 'CacheFirst',
45 | options: {
46 | cacheName: 'google-fonts-cache',
47 | expiration: {
48 | maxEntries: 10,
49 | maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
50 | },
51 | cacheableResponse: {
52 | statuses: [0, 200],
53 | },
54 | },
55 | },
56 | {
57 | urlPattern: /^https:\/\/fonts.gstatic.com\/.*/i,
58 | handler: 'CacheFirst',
59 | options: {
60 | cacheName: 'gstatic-fonts-cache',
61 | expiration: {
62 | maxEntries: 10,
63 | maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
64 | },
65 | cacheableResponse: {
66 | statuses: [0, 200],
67 | },
68 | },
69 | },
70 | ],
71 | },
72 | registerWebManifestInRouteRules: true,
73 | writePlugin: true,
74 | devOptions: {
75 | enabled: process.env.VITE_PLUGIN_PWA === 'true',
76 | navigateFallback: scope,
77 | },
78 | }
79 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const appName = 'Vitesse for Nuxt 3'
2 | export const appDescription = 'Vitesse for Nuxt 3'
3 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/layouts/README.md:
--------------------------------------------------------------------------------
1 | ## Layouts
2 |
3 | Vue components in this dir are used as layouts.
4 |
5 | By default, `default.vue` will be used unless an alternative is specified in the route meta.
6 |
7 | ```html
8 |
13 | ```
14 |
15 | Learn more on https://nuxt.com/docs/guide/directory-structure/layouts
16 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [Default Layout]
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/layouts/home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [Home Layout]
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/netlify.toml:
--------------------------------------------------------------------------------
1 | [build.environment]
2 | NODE_VERSION = "16"
3 |
4 | [build]
5 | publish = "dist"
6 | command = "pnpm run build"
7 |
8 | [[redirects]]
9 | from = "/*"
10 | to = "/index.html"
11 | status = 200
12 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import { pwa } from './config/pwa'
2 | import { appDescription } from './constants/index'
3 |
4 | export default defineNuxtConfig({
5 | modules: [
6 | '@vueuse/nuxt',
7 | '@unocss/nuxt',
8 | '@pinia/nuxt',
9 | '@nuxtjs/color-mode',
10 | '@vite-pwa/nuxt',
11 | ],
12 |
13 | experimental: {
14 | // when using generate, payload js assets included in sw precache manifest
15 | // but missing on offline, disabling extraction it until fixed
16 | payloadExtraction: false,
17 | inlineSSRStyles: false,
18 | renderJsonPayloads: true,
19 | typedPages: true,
20 | },
21 |
22 | css: [
23 | '@unocss/reset/tailwind.css',
24 | ],
25 |
26 | colorMode: {
27 | classSuffix: '',
28 | },
29 |
30 | nitro: {
31 | esbuild: {
32 | options: {
33 | target: 'esnext',
34 | },
35 | },
36 | prerender: {
37 | crawlLinks: false,
38 | routes: ['/'],
39 | ignore: ['/hi'],
40 | },
41 | },
42 |
43 | app: {
44 | head: {
45 | viewport: 'width=device-width,initial-scale=1',
46 | link: [
47 | { rel: 'icon', href: '/favicon.ico', sizes: 'any' },
48 | { rel: 'icon', type: 'image/svg+xml', href: '/nuxt.svg' },
49 | { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' },
50 | ],
51 | meta: [
52 | { name: 'viewport', content: 'width=device-width, initial-scale=1' },
53 | { name: 'description', content: appDescription },
54 | { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
55 | ],
56 | },
57 | },
58 |
59 | pwa,
60 |
61 | devtools: {
62 | enabled: true,
63 | },
64 | })
65 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "packageManager": "pnpm@8.6.3",
4 | "scripts": {
5 | "build": "nuxi build",
6 | "dev": "nuxi dev",
7 | "dev:pwa": "VITE_PLUGIN_PWA=true nuxi dev",
8 | "start": "node .output/server/index.mjs",
9 | "typecheck": "vue-tsc --noEmit",
10 | "lint": "eslint .",
11 | "postinstall": "nuxi prepare",
12 | "generate": "nuxi generate",
13 | "start:generate": "npx serve .output/public"
14 | },
15 | "devDependencies": {
16 | "@antfu/eslint-config": "^0.39.5",
17 | "@iconify-json/carbon": "^1.1.18",
18 | "@iconify-json/twemoji": "^1.1.11",
19 | "@nuxt/devtools": "^0.6.2",
20 | "@nuxtjs/color-mode": "^3.3.0",
21 | "@pinia/nuxt": "^0.4.11",
22 | "@unocss/eslint-config": "^0.53.1",
23 | "@unocss/nuxt": "^0.53.1",
24 | "@vite-pwa/nuxt": "^0.1.0",
25 | "@vueuse/nuxt": "^10.2.0",
26 | "consola": "^3.1.0",
27 | "eslint": "^8.43.0",
28 | "nuxt": "^3.5.3",
29 | "pinia": "^2.1.4",
30 | "typescript": "^5.1.3",
31 | "vue-tsc": "^1.8.1"
32 | },
33 | "dependencies": {
34 | "ipad-cursor": "^0.4.3"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/pages/[...all].vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
10 | Not found
11 |
12 |
13 | Back
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/pages/hi/[id].vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
23 |
24 | Hi,
25 |
26 |
27 | {{ name }}!
28 |
29 |
30 |
31 |
32 | Also as known as:
33 |
34 |
35 |
36 | {{ otherName }}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
51 | Back
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/pages/index.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | You're offline
18 |
19 |
20 |
21 |
22 | Loading...
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/plugins/cursor-directive.ts:
--------------------------------------------------------------------------------
1 | import { ipadCursorPlugin } from '../../../src/vue/index'
2 |
3 | export default defineNuxtPlugin((nuxtApp) => {
4 | // Doing something with nuxtApp
5 | nuxtApp.vueApp.use(ipadCursorPlugin, {
6 | blockStyle: {
7 | radius: 'auto',
8 | },
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/examples/nuxt-basic/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/examples/nuxt-basic/public/favicon.ico
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/maskable-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/examples/nuxt-basic/public/maskable-icon.png
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/nuxt.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/pwa-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/examples/nuxt-basic/public/pwa-192x192.png
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/pwa-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/examples/nuxt-basic/public/pwa-512x512.png
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/public/vite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/examples/nuxt-basic/public/vite.png
--------------------------------------------------------------------------------
/examples/nuxt-basic/server/api/pageview.ts:
--------------------------------------------------------------------------------
1 | const startAt = Date.now()
2 | let count = 0
3 |
4 | export default defineEventHandler(() => ({
5 | pageview: count++,
6 | startAt,
7 | }))
8 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.nuxt/tsconfig.server.json"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/nuxt-basic/uno.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineConfig,
3 | presetAttributify,
4 | presetIcons,
5 | presetTypography,
6 | presetUno,
7 | presetWebFonts,
8 | transformerDirectives,
9 | transformerVariantGroup,
10 | } from 'unocss'
11 |
12 | export default defineConfig({
13 | shortcuts: [
14 | ['btn', 'px-4 py-1 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
15 | ['icon-btn', 'inline-block cursor-pointer select-none opacity-75 transition duration-200 ease-in-out hover:opacity-100 hover:text-teal-600'],
16 | ],
17 | presets: [
18 | presetUno(),
19 | presetAttributify(),
20 | presetIcons({
21 | scale: 1.2,
22 | }),
23 | presetTypography(),
24 | presetWebFonts({
25 | fonts: {
26 | sans: 'DM Sans',
27 | serif: 'DM Serif Display',
28 | mono: 'DM Mono',
29 | },
30 | }),
31 | ],
32 | transformers: [
33 | transformerDirectives(),
34 | transformerVariantGroup(),
35 | ],
36 | })
37 |
--------------------------------------------------------------------------------
/examples/react-basic/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | root: true,
5 | env: { browser: true, es2020: true },
6 | extends: [
7 | 'eslint:recommended',
8 | 'plugin:@typescript-eslint/recommended',
9 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
10 | 'plugin:react-hooks/recommended',
11 | ],
12 | parser: '@typescript-eslint/parser',
13 | parserOptions: {
14 | ecmaVersion: 'latest',
15 | sourceType: 'module',
16 | project: true,
17 | tsconfigRootDir: __dirname,
18 | },
19 | plugins: ['react-refresh'],
20 | rules: {
21 | 'react-refresh/only-export-components': [
22 | 'warn',
23 | { allowConstantExport: true },
24 | ],
25 | '@typescript-eslint/no-non-null-assertion': 'off',
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/examples/react-basic/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/examples/react-basic/.npmrc:
--------------------------------------------------------------------------------
1 | ipad-cursor:registry=https://registry.npmjs.org
--------------------------------------------------------------------------------
/examples/react-basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/react-basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-basic",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "ipad-cursor": "0.4.3",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.2.14",
19 | "@types/react-dom": "^18.2.6",
20 | "@typescript-eslint/eslint-plugin": "^5.61.0",
21 | "@typescript-eslint/parser": "^5.61.0",
22 | "@vitejs/plugin-react": "^4.0.1",
23 | "eslint": "^8.44.0",
24 | "eslint-plugin-react-hooks": "^4.6.0",
25 | "eslint-plugin-react-refresh": "^0.4.1",
26 | "typescript": "^5.0.2",
27 | "unocss": "^0.53.5",
28 | "vite": "^4.4.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/react-basic/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/react-basic/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/examples/react-basic/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import reactLogo from "./assets/react.svg";
3 | import viteLogo from "/vite.svg";
4 | import "./App.css";
5 |
6 | import { IPadCursorProvider, useIPadCursor } from "ipad-cursor/react";
7 | import { IpadCursorConfig } from "ipad-cursor";
8 |
9 | function App() {
10 | const config: IpadCursorConfig = {
11 | blockPadding: "auto",
12 | blockStyle: {
13 | radius: "auto",
14 | },
15 | enableAutoTextCursor: true,
16 | };
17 | useIPadCursor();
18 | const [count, setCount] = useState(0);
19 |
20 | return (
21 |
22 |
28 |
29 |
30 |
36 |
37 |
38 | Vite + React
39 |
40 |
setCount((count) => count + 1)}>
41 | count is {count}
42 |
43 |
44 | Edit src/App.tsx
and save to test HMR
45 |
46 |
47 |
48 | Click on the Vite and React logos to learn more
49 |
50 |
51 | );
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/examples/react-basic/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/react-basic/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | a {
18 | font-weight: 500;
19 | color: #646cff;
20 | text-decoration: inherit;
21 | }
22 | a:hover {
23 | color: #535bf2;
24 | }
25 |
26 | body {
27 | margin: 0;
28 | display: flex;
29 | place-items: center;
30 | min-width: 320px;
31 | min-height: 100vh;
32 | }
33 |
34 | h1 {
35 | font-size: 3.2em;
36 | line-height: 1.1;
37 | }
38 |
39 | button {
40 | border-radius: 8px;
41 | border: 1px solid transparent;
42 | padding: 0.6em 1.2em;
43 | font-size: 1em;
44 | font-weight: 500;
45 | font-family: inherit;
46 | background-color: #1a1a1a;
47 | cursor: pointer;
48 | transition: border-color 0.25s;
49 | }
50 | button:hover {
51 | border-color: #646cff;
52 | }
53 | button:focus,
54 | button:focus-visible {
55 | outline: 4px auto -webkit-focus-ring-color;
56 | }
57 |
58 | @media (prefers-color-scheme: light) {
59 | :root {
60 | color: #213547;
61 | background-color: #ffffff;
62 | }
63 | a:hover {
64 | color: #747bff;
65 | }
66 | button {
67 | background-color: #f9f9f9;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/examples/react-basic/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 | import 'virtual:uno.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root')!).render(
8 |
9 |
10 | ,
11 | )
12 |
--------------------------------------------------------------------------------
/examples/react-basic/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/react-basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/examples/react-basic/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/examples/react-basic/uno.config.ts:
--------------------------------------------------------------------------------
1 | // uno.config.ts
2 | import {
3 | defineConfig,
4 | presetAttributify,
5 | presetIcons,
6 | presetTypography,
7 | presetUno,
8 | transformerDirectives,
9 | transformerVariantGroup,
10 | } from "unocss";
11 |
12 | export default defineConfig({
13 | // ...UnoCSS options
14 | shortcuts: [
15 | ['btn', 'px4 py1 rounded-md whitespace-nowrap bg-gray/20 hover:bg-gray/30 font-500'],
16 | ['icon-btn', 'p3 rounded-md'],
17 | ['full', 'w-full h-full'],
18 | ['flex-center', 'flex items-center justify-center'],
19 | ],
20 | presets: [
21 | presetUno(),
22 | presetAttributify(),
23 | presetIcons({
24 | scale: 1.2,
25 | autoInstall: true,
26 | }),
27 | presetTypography(),
28 | ],
29 | transformers: [transformerDirectives(), transformerVariantGroup()],
30 | });
31 |
--------------------------------------------------------------------------------
/examples/react-basic/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import UnoCSS from "unocss/vite"
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react(), UnoCSS()],
8 | })
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ipad-cursor",
3 | "version": "0.5.2",
4 | "description": "Mouse effect of iPad in browser that can be used in any framework",
5 | "scripts": {
6 | "dev": "cd ./playground && vite",
7 | "build": "node ./.build.mjs",
8 | "build:playground": "cd ./playground && vite-ssg build -c ./vite.config.ts",
9 | "publish": "node ./.build.mjs --publish",
10 | "build:react-ts": "cd ./src/react && tsc"
11 | },
12 | "type": "module",
13 | "main": "index.mjs",
14 | "module": "index.mjs",
15 | "types": "index.d.ts",
16 | "exports": {
17 | "./vue": {
18 | "import": "./vue/index.mjs",
19 | "types": "./vue/index.d.ts",
20 | "default": "./vue/index.mjs"
21 | },
22 | "./react": {
23 | "import": "./react/index.mjs",
24 | "types": "./react/index.d.ts",
25 | "default": "./react/index.mjs"
26 | },
27 | ".": {
28 | "import": "./index.mjs",
29 | "types": "./index.d.ts",
30 | "default": "./index.mjs"
31 | }
32 | },
33 | "keywords": [
34 | "iPad Cursor",
35 | "Mouse Effect",
36 | "Vue",
37 | "iPad",
38 | "Mouse",
39 | "Cursor"
40 | ],
41 | "author": "CatsJuice",
42 | "repository": "https://github.com/catsjuice/ipad-cursor/",
43 | "license": "MIT",
44 | "devDependencies": {
45 | "@formkit/auto-animate": "^0.7.0",
46 | "@highlightjs/vue-plugin": "^2.1.0",
47 | "@iconify-json/carbon": "^1.1.18",
48 | "@iconify-json/logos": "^1.1.33",
49 | "@iconify-json/vscode-icons": "^1.1.25",
50 | "@rollup/plugin-terser": "^0.4.3",
51 | "@rollup/plugin-typescript": "^11.1.2",
52 | "@types/highlight.js": "^10.1.0",
53 | "@types/node": "^20.3.3",
54 | "@types/react": "^18.2.14",
55 | "@unhead/vue": "^1.1.30",
56 | "@vitejs/plugin-vue": "^4.1.0",
57 | "@vueuse/core": "^10.2.1",
58 | "brotli-size": "^4.0.0",
59 | "bumpp": "^9.1.1",
60 | "chalk": "^5.3.0",
61 | "execa": "^7.1.1",
62 | "highlight.js": "^11.8.0",
63 | "highlightjs-vue": "^1.0.0",
64 | "pretty-bytes": "^6.1.0",
65 | "prompts": "^2.4.2",
66 | "react": "^18.2.0",
67 | "shx": "^0.3.4",
68 | "taze": "^0.11.2",
69 | "tslib": "^2.6.0",
70 | "typescript": "^5.0.2",
71 | "unocss": "^0.53.4",
72 | "url": "^0.11.1",
73 | "vite": "^4.3.9",
74 | "vite-ssg": "^0.23.0",
75 | "vue": "^3.2.47",
76 | "vue-highlight.js": "^3.1.0",
77 | "vue-router": "^4.2.3",
78 | "vue-tsc": "^1.4.2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/playground/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + Vue + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playground/public/ipad-cursor-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/playground/public/ipad-cursor.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/playground/public/og-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/playground/public/og-image.jpg
--------------------------------------------------------------------------------
/playground/public/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CatsJuice/ipad-cursor/34190bd39827e91aff6b6188a39b27695eba23cc/playground/public/screenshot.gif
--------------------------------------------------------------------------------
/playground/src/App.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
75 |
--------------------------------------------------------------------------------
/playground/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playground/src/codes/basic.ts:
--------------------------------------------------------------------------------
1 | import { UNPKG_CDN } from "../constants";
2 | import { Code } from "../types/lang";
3 |
4 | export const basicCodes: Code[] = [
5 | {
6 | lang: "html",
7 | code: `
8 |
9 |
10 |
11 | will be text
12 |
13 |
14 | Button
15 |
16 |
17 |
18 |
22 | `,
23 | },
24 | {
25 | lang: "vue",
26 | code: `
31 |
32 |
33 |
34 |
35 | will be text
36 |
37 |
38 | Button
39 |
40 |
41 | `,
42 | },
43 | {
44 | lang: "react",
45 | code: `// app.tsx
46 | import { IPadCursorProvider, useIPadCursor } from "ipad-cursor/react";
47 | import type { IpadCursorConfig } from "ipad-cursor";
48 |
49 | function App() {
50 | const config: IpadCursorConfig = {};
51 | useIPadCursor();
52 |
53 | return (
54 |
55 |
56 |
57 |
58 | )
59 | }
60 | `
61 | }
62 | ];
63 |
--------------------------------------------------------------------------------
/playground/src/codes/blur-effect.ts:
--------------------------------------------------------------------------------
1 | import { UNPKG_CDN } from "../constants";
2 | import { Code } from "../types/lang";
3 |
4 | export const blurEffectCodes: Code[] = [
5 | {
6 | lang: "html",
7 | code: `
8 |
9 |
10 |
11 | will be text
12 |
13 |
17 | Button
18 |
19 |
20 |
21 |
25 | `,
26 | },
27 | {
28 | lang: "vue",
29 | code: `
38 |
39 |
40 |
41 |
42 | will be text
43 |
44 |
45 | Button
46 |
47 |
48 | `,
49 | },
50 | {
51 | lang: "react",
52 | code: `// app.tsx
53 | import { IPadCursorProvider, useIPadCursor } from "ipad-cursor/react";
54 | import type { IpadCursorConfig } from "ipad-cursor";
55 |
56 | function App() {
57 | const config: IpadCursorConfig = {};
58 | const { customCursorStyle } = useIPadCursor();
59 | const style = customCursorStyle({
60 | backdropBlur: 0,
61 | durationBackdropFilter: 1000,
62 | });
63 |
64 | return (
65 |
66 |
67 | will be text
68 |
69 |
70 | Button
71 |
72 |
73 | )
74 | }
75 | `,
76 | },
77 | ];
78 |
--------------------------------------------------------------------------------
/playground/src/codes/custom-anime-speed.ts:
--------------------------------------------------------------------------------
1 | import { UNPKG_CDN } from "../constants";
2 | import { Code } from "../types/lang";
3 |
4 | export const customAnimeSpeedCodes: Code[] = [
5 | {
6 | lang: "html",
7 | code: `
8 |
9 |
10 |
11 | will be text
12 |
13 |
17 | Button
18 |
19 |
20 |
21 |
25 | `,
26 | },
27 | {
28 | lang: "vue",
29 | code: `
37 |
38 |
39 |
40 |
41 | will be text
42 |
43 |
44 | Button
45 |
46 |
47 | `,
48 | },
49 |
50 | {
51 | lang: "react",
52 | code: `// app.tsx
53 | import { IPadCursorProvider, useIPadCursor } from "ipad-cursor/react";
54 | import type { IpadCursorConfig } from "ipad-cursor";
55 |
56 | function App() {
57 | const config: IpadCursorConfig = {};
58 | const { customCursorStyle } = useIPadCursor();
59 | const style = customCursorStyle({
60 | durationBase: 2000,
61 | });
62 |
63 | return (
64 |
65 |
66 | will be text
67 |
68 |
69 | Button
70 |
71 |
72 | )
73 | }
74 | `,
75 | },
76 | ];
77 |
--------------------------------------------------------------------------------
/playground/src/codes/custom-bg.ts:
--------------------------------------------------------------------------------
1 | import { UNPKG_CDN } from "../constants";
2 | import { Code } from "../types/lang";
3 |
4 | export const customBgCodes: Code[] = [
5 | {
6 | lang: "html",
7 | code: `
8 |
9 |
10 |
11 | will be text
12 |
13 |
14 | Button
15 |
16 |
17 |
18 |
22 | `,
23 | },
24 | {
25 | lang: "vue",
26 | code: `
31 |
32 |
33 |
34 |
35 | will be text
36 |
37 |
38 | Button
39 |
40 |
41 | `,
42 | },
43 | {
44 | lang: "react",
45 | code: `// app.tsx
46 | import { IPadCursorProvider, useIPadCursor } from "ipad-cursor/react";
47 | import type { IpadCursorConfig } from "ipad-cursor";
48 |
49 | function App() {
50 | const config: IpadCursorConfig = {};
51 | useIPadCursor();
52 |
53 | return (
54 |
55 |
56 | will be text
57 |
58 |
59 | Button
60 |
61 |
62 | )
63 | }
64 | `,
65 | },
66 | ];
67 |
--------------------------------------------------------------------------------
/playground/src/codes/custom-border.ts:
--------------------------------------------------------------------------------
1 | import { UNPKG_CDN } from "../constants";
2 | import { Code } from "../types/lang";
3 |
4 | export const customBorderCodes: Code[] = [
5 | {
6 | lang: "html",
7 | code: `
8 |
9 |
10 |
11 | will be text
12 |
13 |
17 | Button
18 |
19 |
20 |
21 |
25 | `,
26 | },
27 | {
28 | lang: "vue",
29 | code: `
37 |
38 |
39 |
40 |
41 | will be text
42 |
43 |
44 | Button
45 |
46 |
47 | `,
48 | },
49 |
50 |
51 | {
52 | lang: "react",
53 | code: `// app.tsx
54 | import { IPadCursorProvider, useIPadCursor } from "ipad-cursor/react";
55 | import type { IpadCursorConfig } from "ipad-cursor";
56 |
57 | function App() {
58 | const config: IpadCursorConfig = {};
59 | const { customCursorStyle } = useIPadCursor();
60 | const style = customCursorStyle({
61 | border: '1px solid currentColor',
62 | });
63 |
64 | return (
65 |
66 |
67 | will be text
68 |
69 |
70 | Button
71 |
72 |
73 | )
74 | }
75 | `,
76 | },
77 | ];
78 |
--------------------------------------------------------------------------------
/playground/src/codes/install.ts:
--------------------------------------------------------------------------------
1 | import { packageName } from "../constants";
2 | import { Code } from "../types/lang";
3 |
4 | export const installCodes: Code[] = [
5 | {
6 | lang: "pnpm",
7 | title: "Pnpm",
8 | code: `pnpm i ${packageName}`,
9 | },
10 | {
11 | lang: "npm",
12 | title: 'NPM',
13 | code: `npm i ${packageName} --save`,
14 | },
15 | {
16 | lang: "yarn",
17 | title: "Yarn",
18 | code: `yarn add ${packageName}`,
19 | },
20 | ];
21 |
--------------------------------------------------------------------------------
/playground/src/components/AppHeader.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
34 |
35 |
--------------------------------------------------------------------------------
/playground/src/components/CodeBox/index.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
46 |
47 |
48 |
62 |
63 |
{{ title }}
64 |
{{ lang }}
65 |
66 |
67 |
68 |
69 |
79 |
84 |
85 |
86 |
103 |
104 |
105 |
106 |
123 |
--------------------------------------------------------------------------------
/playground/src/components/LanguageIcon.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/playground/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const packageName = 'ipad-cursor';
2 | export const UNPKG_CDN = `https://unpkg.com/${packageName}@latest`;
--------------------------------------------------------------------------------
/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ViteSSG } from "vite-ssg";
2 | import "virtual:uno.css";
3 | import "./style.css";
4 | import App from "./App.vue";
5 | import hljsVuePlugin from "@highlightjs/vue-plugin";
6 | import "highlight.js/styles/base16/darcula.css";
7 | import hljs from "highlight.js/lib/core";
8 | import javascript from "highlight.js/lib/languages/javascript";
9 | import typescript from "highlight.js/lib/languages/typescript";
10 | import { autoAnimatePlugin } from "@formkit/auto-animate/vue";
11 | import vue from 'vue-highlight.js/lib/languages/vue';
12 | import { ipadCursorPlugin } from "../../src/vue";
13 |
14 |
15 | hljs.registerLanguage("js", javascript);
16 | hljs.registerLanguage("ts", typescript);
17 | hljs.registerLanguage("vue", vue);
18 |
19 | // createApp(App).mount('#app')
20 | export const createApp = ViteSSG(
21 | App,
22 | {
23 | routes: [{ path: "/", component: () => import("./pages/index.vue") }],
24 | },
25 | (ctx) => {
26 | ctx.app.use(hljsVuePlugin);
27 | ctx.app.use(autoAnimatePlugin);
28 | ctx.app.use(ipadCursorPlugin, {
29 | enableMouseDownEffect: true,
30 | enableAutoTextCursor: true,
31 | enableLighting: true,
32 | normalStyle: {
33 | scale: 1,
34 | },
35 | blockPadding: "auto",
36 | blockStyle: {
37 | radius: "auto",
38 | },
39 | })
40 | }
41 | );
42 |
--------------------------------------------------------------------------------
/playground/src/pages/index.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 |
63 |
64 |
73 |
74 |
80 |
81 |
82 |
83 |
84 |
85 |
90 | iPad Cursor
91 |
92 |
93 |
Hack iPad's mouse effect in browser,
94 |
can be used in any frameworks
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
117 | {{ btn.label }}
118 |
119 |
120 |
121 |
122 |
123 |
128 | Recover mouse
129 |
130 |
131 |
136 | Enable iPad Mouse
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/playground/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | * {
18 | box-sizing: border-box;
19 | margin: 0;
20 | padding: 0;
21 | }
22 |
--------------------------------------------------------------------------------
/playground/src/types/lang.ts:
--------------------------------------------------------------------------------
1 | export type Lang = "js" | "ts" | "vue" | "html" | "yarn" | "npm" | "pnpm" | "react";
2 | export type Code = {
3 | lang: Lang;
4 | code: string;
5 | title?: string;
6 | };
7 |
--------------------------------------------------------------------------------
/playground/src/utils/clipboard.ts:
--------------------------------------------------------------------------------
1 | export function copy2clipboard(text: string) {
2 | const el = document.createElement("textarea");
3 | el.value = text;
4 | el.setAttribute("readonly", "");
5 | el.style.position = "absolute";
6 | el.style.left = "-9999px";
7 | document.body.appendChild(el);
8 | el.select();
9 | document.execCommand("copy");
10 | document.body.removeChild(el);
11 | }
--------------------------------------------------------------------------------
/playground/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/playground/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/playground/uno.config.ts:
--------------------------------------------------------------------------------
1 | // uno.config.ts
2 | import {
3 | defineConfig,
4 | presetAttributify,
5 | presetIcons,
6 | presetTypography,
7 | presetUno,
8 | transformerDirectives,
9 | transformerVariantGroup,
10 | } from "unocss";
11 |
12 | export default defineConfig({
13 | // ...UnoCSS options
14 | shortcuts: [
15 | ['btn', 'px4 py1 rounded-md whitespace-nowrap bg-gray/20 hover:bg-gray/30 font-500'],
16 | ['icon-btn', 'p3 rounded-md'],
17 | ['full', 'w-full h-full'],
18 | ['flex-center', 'flex items-center justify-center'],
19 | ],
20 | presets: [
21 | presetUno(),
22 | presetAttributify(),
23 | presetIcons({
24 | scale: 1.2,
25 | autoInstall: true,
26 | }),
27 | presetTypography(),
28 | ],
29 | transformers: [transformerDirectives(), transformerVariantGroup()],
30 | });
31 |
--------------------------------------------------------------------------------
/playground/vite.config.ts:
--------------------------------------------------------------------------------
1 | import UnoCSS from "unocss/vite";
2 | import { defineConfig } from "vite";
3 | import vue from "@vitejs/plugin-vue";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [vue(), UnoCSS()],
8 | ssr: {
9 | noExternal: ['@highlightjs/vue-plugin']
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from "@rollup/plugin-typescript";
2 | import terser from "@rollup/plugin-terser";
3 |
4 | const FRAMEWORK = process.env.FRAMEWORK || "index";
5 | const DECLARATIONS = process.env.DECLARATIONS || false;
6 | const MIN = process.env.MIN || false;
7 |
8 | const external = ["vue", "react", "react-dom"];
9 |
10 | function createOutput() {
11 | if (DECLARATIONS) {
12 | return {
13 | dir: "./dist",
14 | format: "esm",
15 | };
16 | }
17 | return {
18 | file: `./dist/${FRAMEWORK !== "index" ? FRAMEWORK + "/" : ""}index.${
19 | MIN ? "min.js" : "mjs"
20 | }`,
21 | format: "esm",
22 | };
23 | }
24 |
25 | function getExt() {
26 | if (["react"].includes(FRAMEWORK)) return "tsx";
27 | return "ts";
28 | }
29 |
30 | const plugins = [
31 | typescript({
32 | tsconfig: "tsconfig.json",
33 | compilerOptions: DECLARATIONS
34 | ? {
35 | declaration: true,
36 | emitDeclarationOnly: true,
37 | }
38 | : {},
39 | rootDir: "./",
40 | outDir: `./dist`,
41 | include: ["./src/**/*"],
42 | exclude: ["./playground"],
43 | }),
44 | ];
45 |
46 | if (MIN) {
47 | plugins.push(terser());
48 | }
49 |
50 | export default {
51 | external,
52 | input: `./src/${
53 | FRAMEWORK === "index" ? "" : FRAMEWORK + "/"
54 | }index.${getExt()}`,
55 | output: createOutput(),
56 | plugins,
57 | };
58 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | export type ICursorType = "normal" | "text" | "block";
2 | /**
3 | * if without unit, `px` is used by default
4 | */
5 | type MaybeSize = string | number;
6 | /** if without unit `ms` is used by default */
7 | type MaybeDuration = string | number;
8 | /** do not use 0x000000, use #000000 instead */
9 | type MaybeColor = string;
10 | /**
11 | * Configurations for the cursor
12 | */
13 | export interface IpadCursorConfig {
14 | /**
15 | * Strength of adsorption, the larger the value,
16 | * The higher the value, the greater the range of the block that can be moved when it is hovered
17 | * @type {number} between 0 and 30
18 | * @default 10
19 | */
20 | adsorptionStrength?: number;
21 | /**
22 | * The class name of the cursor element
23 | * @type {string}
24 | * @default 'cursor'
25 | */
26 | className?: string;
27 | /**
28 | * The style of the cursor, when it does not hover on any element
29 | */
30 | normalStyle?: IpadCursorStyle;
31 | /**
32 | * The style of the cursor, when it hovers on text
33 | */
34 | textStyle?: IpadCursorStyle;
35 | /**
36 | * The style of the cursor, when it hovers on a block
37 | */
38 | blockStyle?: IpadCursorStyle;
39 | /**
40 | * The style of the cursor, when mousedown
41 | */
42 | mouseDownStyle?: IpadCursorStyle;
43 | /**
44 | * Cursor padding when hover on block
45 | */
46 | blockPadding?: number | "auto";
47 | /**
48 | * detect text node and apply text cursor automatically
49 | **/
50 | enableAutoTextCursor?: boolean;
51 | /**
52 | * enable detect dom change and auto call updateCursor
53 | **/
54 | enableAutoUpdateCursor?: boolean;
55 | /**
56 | * whether to enable lighting effect
57 | */
58 | enableLighting?: boolean;
59 | /**
60 | * whether to apply effect for mousedown action
61 | */
62 | enableMouseDownEffect?: boolean;
63 | }
64 | /**
65 | * Configurable style of the cursor (Experimental)
66 | * This feature is Experimental, so it's set to false by default.
67 | * And it not support `block` yet
68 | */
69 | export interface IpadCursorStyle {
70 | /**
71 | * The width of the cursor
72 | */
73 | width?: MaybeSize;
74 | /**
75 | * The width of the cursor
76 | */
77 | height?: MaybeSize;
78 | /**
79 | * Border radius of cursor
80 | */
81 | radius?: MaybeSize | "auto";
82 | /**
83 | * Transition duration of basic properties like width, height, radius, border, background-color
84 | */
85 | durationBase?: MaybeDuration;
86 | /**
87 | * Transition duration of position: left, top
88 | */
89 | durationPosition?: MaybeDuration;
90 | /**
91 | * Transition duration of backdrop-filter
92 | */
93 | durationBackdropFilter?: MaybeDuration;
94 | /**
95 | * The background color of the cursor
96 | */
97 | background?: MaybeColor;
98 | /**
99 | * Border of the cursor
100 | * @example '1px solid rgba(100, 100, 100, 0.1)'
101 | */
102 | border?: string;
103 | /** z-index of cursor */
104 | zIndex?: number;
105 | /**
106 | * Scale of cursor
107 | */
108 | scale?: number;
109 | /**
110 | * backdrop-filter blur
111 | */
112 | backdropBlur?: MaybeSize;
113 | /**
114 | * backdrop-filter saturate
115 | */
116 | backdropSaturate?: string;
117 | }
118 | /**
119 | * Init cursor, hide default cursor, and listen mousemove event
120 | * will only run once in client even if called multiple times
121 | * @returns
122 | */
123 | declare function initCursor(_config?: IpadCursorConfig): void;
124 | /**
125 | * destroy cursor, remove event listener and remove cursor element
126 | * @returns
127 | */
128 | declare function disposeCursor(): void;
129 | /**
130 | * Update current Configuration
131 | * @param _config
132 | */
133 | declare function updateConfig(_config: IpadCursorConfig): IpadCursorConfig;
134 | /**
135 | * Detect all interactive elements in the page
136 | * Update the binding of events, remove listeners for elements that are removed
137 | * @returns
138 | */
139 | declare function updateCursor(): void;
140 | /**
141 | * Create custom style that can be bound to `data-cursor-style`
142 | * @param style
143 | */
144 | declare function customCursorStyle(style: IpadCursorStyle & Record): string;
145 | declare function resetCursor(): void;
146 | declare const CursorType: {
147 | TEXT: ICursorType;
148 | BLOCK: ICursorType;
149 | };
150 | declare const exported: {
151 | CursorType: {
152 | TEXT: ICursorType;
153 | BLOCK: ICursorType;
154 | };
155 | resetCursor: typeof resetCursor;
156 | initCursor: typeof initCursor;
157 | updateCursor: typeof updateCursor;
158 | disposeCursor: typeof disposeCursor;
159 | updateConfig: typeof updateConfig;
160 | customCursorStyle: typeof customCursorStyle;
161 | };
162 | export { CursorType, resetCursor, initCursor, updateCursor, disposeCursor, updateConfig, customCursorStyle, };
163 | export default exported;
164 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // types
2 | export type ICursorType = "normal" | "text" | "block";
3 | /**
4 | * if without unit, `px` is used by default
5 | */
6 | type MaybeSize = string | number;
7 | /** if without unit `ms` is used by default */
8 | type MaybeDuration = string | number;
9 | /** do not use 0x000000, use #000000 instead */
10 | type MaybeColor = string;
11 |
12 | /**
13 | * Configurations for the cursor
14 | */
15 | export interface IpadCursorConfig {
16 | /**
17 | * Strength of adsorption, the larger the value,
18 | * The higher the value, the greater the range of the block that can be moved when it is hovered
19 | * @type {number} between 0 and 30
20 | * @default 10
21 | */
22 | adsorptionStrength?: number;
23 |
24 | /**
25 | * The class name of the cursor element
26 | * @type {string}
27 | * @default 'cursor'
28 | */
29 | className?: string;
30 |
31 | /**
32 | * The style of the cursor, when it does not hover on any element
33 | */
34 | normalStyle?: IpadCursorStyle;
35 | /**
36 | * The style of the cursor, when it hovers on text
37 | */
38 | textStyle?: IpadCursorStyle;
39 | /**
40 | * The style of the cursor, when it hovers on a block
41 | */
42 | blockStyle?: IpadCursorStyle;
43 | /**
44 | * The style of the cursor, when mousedown
45 | */
46 | mouseDownStyle?: IpadCursorStyle;
47 | /**
48 | * Cursor padding when hover on block
49 | */
50 | blockPadding?: number | "auto";
51 |
52 | /**
53 | * detect text node and apply text cursor automatically
54 | **/
55 | enableAutoTextCursor?: boolean;
56 |
57 | /**
58 | * enable detect dom change and auto call updateCursor
59 | **/
60 | enableAutoUpdateCursor?: boolean;
61 | /**
62 | * whether to enable lighting effect
63 | */
64 | enableLighting?: boolean;
65 |
66 | /**
67 | * whether to apply effect for mousedown action
68 | */
69 | enableMouseDownEffect?: boolean;
70 | }
71 | /**
72 | * Configurable style of the cursor (Experimental)
73 | * This feature is Experimental, so it's set to false by default.
74 | * And it not support `block` yet
75 | */
76 | export interface IpadCursorStyle {
77 | /**
78 | * The width of the cursor
79 | */
80 | width?: MaybeSize;
81 | /**
82 | * The width of the cursor
83 | */
84 | height?: MaybeSize;
85 | /**
86 | * Border radius of cursor
87 | */
88 | radius?: MaybeSize | "auto";
89 |
90 | /**
91 | * Transition duration of basic properties like width, height, radius, border, background-color
92 | */
93 | durationBase?: MaybeDuration;
94 | /**
95 | * Transition duration of position: left, top
96 | */
97 | durationPosition?: MaybeDuration;
98 | /**
99 | * Transition duration of backdrop-filter
100 | */
101 | durationBackdropFilter?: MaybeDuration;
102 | /**
103 | * The background color of the cursor
104 | */
105 | background?: MaybeColor;
106 | /**
107 | * Border of the cursor
108 | * @example '1px solid rgba(100, 100, 100, 0.1)'
109 | */
110 | border?: string;
111 |
112 | /** z-index of cursor */
113 | zIndex?: number;
114 |
115 | /**
116 | * Scale of cursor
117 | */
118 | scale?: number;
119 |
120 | /**
121 | * backdrop-filter blur
122 | */
123 | backdropBlur?: MaybeSize;
124 |
125 | /**
126 | * backdrop-filter saturate
127 | */
128 | backdropSaturate?: string;
129 | }
130 |
131 | let ready = false;
132 | let observer: MutationObserver | null = null;
133 | let cursorEle: HTMLDivElement | null = null;
134 | let activeDom: Element | null = null;
135 | let isBlockActive = false;
136 | let isTextActive = false;
137 | let isMouseDown = false;
138 | let styleTag: HTMLStyleElement | null = null;
139 | let latestCursorStyle: Record = {};
140 | let mousedownStyleRecover: Record = {};
141 | const position = { x: -100, y: -100};
142 | const isServer = typeof document === "undefined";
143 | const registeredNodeSet = new Set();
144 | const eventMap = new Map<
145 | Element,
146 | Array<{ event: string; handler: (e: Event) => void }>
147 | >();
148 | const config = getDefaultConfig();
149 |
150 | /**
151 | * Util collection
152 | */
153 | class Utils {
154 | static clamp(num: number, min: number, max: number) {
155 | return Math.min(Math.max(num, min), max);
156 | }
157 | static isNum(v: string | number) {
158 | return typeof v === "number" || /^\d+$/.test(v);
159 | }
160 | static getSize(size: MaybeSize) {
161 | if (this.isNum(size)) return `${size}px`;
162 | return size;
163 | }
164 | static getDuration(duration: MaybeDuration): string {
165 | if (this.isNum(duration)) return `${duration}ms`;
166 | return `${duration}`;
167 | }
168 | static getColor(color: MaybeColor) {
169 | return color;
170 | }
171 | static objectKeys(obj: Partial>): T[] {
172 | return Object.keys(obj) as T[];
173 | }
174 | static style2Vars(style: IpadCursorStyle) {
175 | const map: Record = {
176 | backdropBlur: "--cursor-bg-blur",
177 | backdropSaturate: "--cursor-bg-saturate",
178 | background: "--cursor-bg",
179 | border: "--cursor-border",
180 | durationBackdropFilter: "--cursor-blur-duration",
181 | durationBase: "--cursor-duration",
182 | durationPosition: "--cursor-position-duration",
183 | height: "--cursor-height",
184 | radius: "--cursor-radius",
185 | scale: "--cursor-scale",
186 | width: "--cursor-width",
187 | zIndex: "--cursor-z-index",
188 | };
189 | return this.objectKeys(style).reduce((prev, key) => {
190 | let value = style[key];
191 | if (value === undefined) return prev;
192 |
193 | const maybeColor = ["background", "border"].includes(key);
194 | const maybeSize = ["width", "height", "radius", "backdropBlur"].includes(
195 | key
196 | );
197 | const maybeDuration = key.startsWith("duration");
198 |
199 | if (maybeColor) value = this.getColor(value as MaybeColor);
200 | if (maybeSize) value = this.getSize(value as MaybeSize);
201 | if (maybeDuration) value = this.getDuration(value as MaybeDuration);
202 |
203 | const recordKey = map[key] || key;
204 | return { ...prev, [recordKey]: value };
205 | }, {});
206 | }
207 | static isMergebleObject(obj: any) {
208 | const isObject = (o: any) =>
209 | o && typeof o === "object" && !Array.isArray(o);
210 | return isObject(obj);
211 | }
212 | static mergeDeep(obj: T, ...sources: any[]): T {
213 | if (!sources.length) return obj;
214 | const source = sources.shift();
215 | if (!source) return obj;
216 | if (this.isMergebleObject(obj) && this.isMergebleObject(source)) {
217 | Utils.objectKeys(source).forEach((key) => {
218 | if (this.isMergebleObject(source[key])) {
219 | if (!(obj as any)[key]) Object.assign(obj as any, { [key]: {} });
220 | this.mergeDeep((obj as any)[key], source[key]);
221 | } else {
222 | Object.assign(obj as any, { [key]: source[key] });
223 | }
224 | });
225 | }
226 | return this.mergeDeep(obj, ...sources);
227 | }
228 | }
229 |
230 | /**
231 | * Get default config
232 | * @returns
233 | */
234 | function getDefaultConfig(): IpadCursorConfig {
235 | const normalStyle: IpadCursorStyle = {
236 | width: "20px",
237 | height: "20px",
238 | radius: "10px",
239 | durationBase: "0.23s",
240 | durationPosition: "0s",
241 | durationBackdropFilter: "0s",
242 | background: "rgba(150, 150, 150, 0.2)",
243 | scale: 1,
244 | border: "1px solid rgba(100, 100, 100, 0.1)",
245 | zIndex: 9999,
246 | backdropBlur: "0px",
247 | backdropSaturate: "180%",
248 | };
249 |
250 | const textStyle: IpadCursorStyle = {
251 | background: "rgba(100, 100, 100, 0.3)",
252 | scale: 1,
253 | width: "4px",
254 | height: "1.2em",
255 | border: "0px solid rgba(100, 100, 100, 0)",
256 | durationBackdropFilter: "1s",
257 | radius: "10px",
258 | };
259 |
260 | const blockStyle: IpadCursorStyle = {
261 | background: "rgba(100, 100, 100, 0.3)",
262 | border: "1px solid rgba(100, 100, 100, 0.05)",
263 | backdropBlur: "0px",
264 | durationBase: "0.23s",
265 | durationBackdropFilter: "0.1s",
266 | backdropSaturate: "120%",
267 | radius: "10px",
268 | };
269 |
270 | const mouseDownStyle: IpadCursorStyle = {
271 | background: "rgba(150, 150, 150, 0.3)",
272 | scale: 0.8,
273 | };
274 | const defaultConfig: IpadCursorConfig = {
275 | blockPadding: "auto",
276 | adsorptionStrength: 10,
277 | className: "ipad-cursor",
278 | normalStyle,
279 | textStyle,
280 | blockStyle,
281 | mouseDownStyle,
282 | };
283 | return defaultConfig;
284 | }
285 |
286 | /** update cursor style (single or multiple) */
287 | function updateCursorStyle(
288 | keyOrObj: string | Record,
289 | value?: string
290 | ) {
291 | if (!cursorEle) return;
292 | if (typeof keyOrObj === "string") {
293 | latestCursorStyle[keyOrObj] = value;
294 | value && cursorEle.style.setProperty(keyOrObj, value);
295 | } else {
296 | Object.entries(keyOrObj).forEach(([key, value]) => {
297 | cursorEle && cursorEle.style.setProperty(key, value);
298 | latestCursorStyle[key] = value;
299 | });
300 | }
301 | }
302 |
303 | /** record mouse position */
304 | function onMousemove(e: MouseEvent) {
305 | position.x = e.clientX;
306 | position.y = e.clientY;
307 | autoApplyTextCursor(e.target as HTMLElement);
308 | }
309 |
310 | function onMousedown() {
311 | if (isMouseDown || !config.enableMouseDownEffect || isBlockActive) return;
312 | isMouseDown = true;
313 | mousedownStyleRecover = { ...latestCursorStyle };
314 | updateCursorStyle(Utils.style2Vars(config.mouseDownStyle || {}));
315 | }
316 |
317 | function onMouseup() {
318 | if (!isMouseDown || !config.enableMouseDownEffect || isBlockActive) return;
319 | isMouseDown = false;
320 | const target = mousedownStyleRecover;
321 | const styleToRecover = Utils.objectKeys(
322 | Utils.style2Vars(config.mouseDownStyle || {})
323 | ).reduce((prev, curr) => ({ ...prev, [curr]: target[curr] }), {});
324 | updateCursorStyle(styleToRecover);
325 | }
326 |
327 | /**
328 | * Automatically apply cursor style when hover on target
329 | * @param target
330 | * @returns
331 | */
332 | function autoApplyTextCursor(target: HTMLElement) {
333 | if (isBlockActive || isTextActive || !config.enableAutoTextCursor) return;
334 | if (target && target.childNodes.length === 1) {
335 | const child = target.childNodes[0] as HTMLElement;
336 | if (child.nodeType === 3 && child.textContent?.trim() !== "") {
337 | target.setAttribute("data-cursor", "text");
338 | applyTextCursor(target);
339 | return;
340 | }
341 | }
342 | resetCursorStyle();
343 | }
344 |
345 | let lastNode: Element | null = null;
346 | const scrollHandler = () => {
347 | const currentNode = document.elementFromPoint(position.x, position.y);
348 | const mouseLeaveEvent = new MouseEvent("mouseleave", {
349 | bubbles: true,
350 | cancelable: true,
351 | view: window,
352 | });
353 | if (currentNode !== lastNode && lastNode && mouseLeaveEvent) {
354 | lastNode.dispatchEvent(mouseLeaveEvent);
355 | }
356 | lastNode = currentNode;
357 | };
358 |
359 | /**
360 | * Init cursor, hide default cursor, and listen mousemove event
361 | * will only run once in client even if called multiple times
362 | * @returns
363 | */
364 | function initCursor(_config?: IpadCursorConfig) {
365 | if (isServer || ready) return;
366 | if (_config) updateConfig(_config);
367 | ready = true;
368 | window.addEventListener("mousemove", onMousemove);
369 | window.addEventListener("mousedown", onMousedown);
370 | window.addEventListener("mouseup", onMouseup);
371 | window.addEventListener("scroll", scrollHandler);
372 | createCursor();
373 | createStyle();
374 | updateCursorPosition();
375 | updateCursor();
376 | createObserver();
377 | }
378 |
379 | function createObserver() {
380 | if (config.enableAutoUpdateCursor) {
381 | observer = new MutationObserver(function () {
382 | updateCursor();
383 | });
384 | observer.observe(document.body, { childList: true, subtree: true });
385 | }
386 | }
387 |
388 | /**
389 | * destroy cursor, remove event listener and remove cursor element
390 | * @returns
391 | */
392 | function disposeCursor() {
393 | if (!ready) return;
394 | ready = false;
395 | window.removeEventListener("mousemove", onMousemove);
396 | window.removeEventListener("scroll", scrollHandler);
397 | cursorEle && cursorEle.remove();
398 | styleTag && styleTag.remove();
399 | styleTag = null;
400 | cursorEle = null;
401 |
402 | // iterate nodesMap
403 | registeredNodeSet.forEach((node) => unregisterNode(node));
404 | observer?.disconnect()
405 | }
406 |
407 | /**
408 | * Update current Configuration
409 | * @param _config
410 | */
411 | function updateConfig(_config: IpadCursorConfig) {
412 | if ("adsorptionStrength" in _config) {
413 | config.adsorptionStrength = Utils.clamp(
414 | _config.adsorptionStrength ?? 10,
415 | 0,
416 | 30
417 | );
418 | }
419 | const newConfig = Utils.mergeDeep(config, _config);
420 | if (!isBlockActive && !isTextActive && _config.normalStyle) {
421 | updateCursorStyle(Utils.style2Vars(newConfig.normalStyle!));
422 | } else if (isBlockActive && _config.blockStyle) {
423 | updateCursorStyle(Utils.style2Vars(newConfig.blockStyle!));
424 | } else if (isTextActive && _config.textStyle) {
425 | updateCursorStyle(Utils.style2Vars(newConfig.textStyle!));
426 | }
427 | return newConfig;
428 | }
429 |
430 | /**
431 | * Create style tag
432 | * @returns
433 | */
434 | function createStyle() {
435 | if (styleTag) return;
436 | const selector = `.${config.className!.split(/\s+/).join(".")}`;
437 | styleTag = document.createElement("style");
438 | styleTag.innerHTML = `
439 | body, * {
440 | cursor: none;
441 | }
442 | ${selector} {
443 | --cursor-transform-duration: 0.23s;
444 | overflow: hidden;
445 | pointer-events: none;
446 | position: fixed;
447 | left: var(--cursor-x);
448 | top: var(--cursor-y);
449 | width: var(--cursor-width);
450 | height: var(--cursor-height);
451 | border-radius: var(--cursor-radius);
452 | background-color: var(--cursor-bg);
453 | border: var(--cursor-border);
454 | z-index: var(--cursor-z-index);
455 | font-size: var(--cursor-font-size);
456 | backdrop-filter:
457 | blur(var(--cursor-bg-blur))
458 | saturate(var(--cursor-bg-saturate));
459 | transition:
460 | width var(--cursor-duration) ease,
461 | height var(--cursor-duration) ease,
462 | border-radius var(--cursor-duration) ease,
463 | border var(--cursor-duration) ease,
464 | background-color var(--cursor-duration) ease,
465 | left var(--cursor-position-duration) ease,
466 | top var(--cursor-position-duration) ease,
467 | backdrop-filter var(--cursor-blur-duration) ease,
468 | transform var(--cursor-transform-duration) ease;
469 | transform:
470 | translateX(calc(var(--cursor-translateX, 0px) - 50%))
471 | translateY(calc(var(--cursor-translateY, 0px) - 50%))
472 | scale(var(--cursor-scale, 1));
473 | }
474 | ${selector}.block-active {
475 | --cursor-transform-duration: 0s;
476 | }
477 | ${selector} .lighting {
478 | display: none;
479 | }
480 | ${selector}.lighting--on .lighting {
481 | display: block;
482 | width: 0;
483 | height: 0;
484 | position: absolute;
485 | left: calc(var(--lighting-size) / -2);
486 | top: calc(var(--lighting-size) / -2);
487 | transform: translateX(var(--lighting-offset-x, 0)) translateY(var(--lighting-offset-y, 0));
488 | background-image: radial-gradient(
489 | circle at center,
490 | rgba(255, 255, 255, 0.1) 0%,
491 | rgba(255, 255, 255, 0) 30%
492 | );
493 | border-radius: 50%;
494 | }
495 | ${selector}.block-active .lighting {
496 | width: var(--lighting-size, 20px);
497 | height: var(--lighting-size, 20px);
498 | }
499 | `;
500 | document.head.appendChild(styleTag);
501 | }
502 |
503 | /**
504 | * create cursor element, append to body
505 | * @returns
506 | */
507 | function createCursor() {
508 | if (isServer) return;
509 | cursorEle = document.createElement("div");
510 | const lightingEle = document.createElement("div");
511 | cursorEle.classList.add(config.className!);
512 | lightingEle.classList.add("lighting");
513 | cursorEle.appendChild(lightingEle);
514 | document.body.appendChild(cursorEle);
515 | resetCursorStyle();
516 | }
517 |
518 | /**
519 | * update cursor position, request animation frame
520 | * @returns
521 | */
522 | function updateCursorPosition() {
523 | if (isServer || !cursorEle) return;
524 | if (!isBlockActive) {
525 | updateCursorStyle("--cursor-x", `${position.x}px`);
526 | updateCursorStyle("--cursor-y", `${position.y}px`);
527 | }
528 | window.requestAnimationFrame(updateCursorPosition);
529 | }
530 |
531 | /**
532 | * get all hover targets
533 | * @returns
534 | */
535 | function queryAllTargets() {
536 | if (isServer || !ready) return [];
537 | return document.querySelectorAll("[data-cursor]");
538 | }
539 |
540 | /**
541 | * Detect all interactive elements in the page
542 | * Update the binding of events, remove listeners for elements that are removed
543 | * @returns
544 | */
545 | function updateCursor() {
546 | initCursor()
547 | if (isServer || !ready) return;
548 | const nodesMap = new Map();
549 | // addDataCursorText(document.body.childNodes)
550 | const nodes = queryAllTargets();
551 | nodes.forEach((node) => {
552 | nodesMap.set(node, true);
553 | if (registeredNodeSet.has(node)) return;
554 | registerNode(node);
555 | });
556 |
557 | registeredNodeSet.forEach((node) => {
558 | if (nodesMap.has(node)) return;
559 | unregisterNode(node);
560 | });
561 | }
562 |
563 | function registerNode(node: Element) {
564 | let type = node.getAttribute("data-cursor") as ICursorType;
565 | registeredNodeSet.add(node);
566 | if (type === "text") registerTextNode(node);
567 | if (type === "block") registerBlockNode(node);
568 | else registeredNodeSet.delete(node);
569 | }
570 |
571 | function unregisterNode(node: Element) {
572 | registeredNodeSet.delete(node);
573 | eventMap.get(node)?.forEach(({ event, handler }: any) => {
574 | if (event === 'mouseleave')
575 | handler();
576 | node.removeEventListener(event, handler);
577 | });
578 | eventMap.delete(node);
579 | (node as HTMLElement).style.setProperty("transform", "none");
580 | }
581 |
582 | function extractCustomStyle(node: Element) {
583 | const customStyleRaw = node.getAttribute("data-cursor-style");
584 | const styleObj: Record = {};
585 | if (customStyleRaw) {
586 | customStyleRaw.split(/(;)/).forEach((style) => {
587 | const [key, value] = style.split(":").map((s) => s.trim());
588 | styleObj[key] = value;
589 | });
590 | }
591 | return styleObj;
592 | }
593 |
594 | /**
595 | * + ---------------------- +
596 | * | TextNode |
597 | * + ---------------------- +
598 | */
599 | function registerTextNode(node: Element) {
600 | let timer: any;
601 |
602 | function toggleTextActive(active?: boolean) {
603 | isTextActive = !!active;
604 | cursorEle &&
605 | (active
606 | ? cursorEle.classList.add("text-active")
607 | : cursorEle.classList.remove("text-active"));
608 | }
609 | function onTextOver(e: Event) {
610 | timer && clearTimeout(timer);
611 | toggleTextActive(true);
612 | // for some edge case, two ele very close
613 | timer = setTimeout(() => toggleTextActive(true));
614 | applyTextCursor(e.target as HTMLElement);
615 | }
616 | function onTextLeave() {
617 | timer && clearTimeout(timer);
618 | timer = setTimeout(() => toggleTextActive(false));
619 | resetCursorStyle();
620 | }
621 | node.addEventListener("mouseover", onTextOver, { passive: true });
622 | node.addEventListener("mouseleave", onTextLeave, { passive: true });
623 | eventMap.set(node, [
624 | { event: "mouseover", handler: onTextOver },
625 | { event: "mouseleave", handler: onTextLeave },
626 | ]);
627 | }
628 |
629 | /**
630 | * + ---------------------- +
631 | * | BlockNode |
632 | * + ---------------------- +
633 | */
634 | function registerBlockNode(_node: Element) {
635 | const node = _node as HTMLElement;
636 | node.addEventListener("mouseenter", onBlockEnter, { passive: true });
637 | node.addEventListener("mousemove", onBlockMove, { passive: true });
638 | node.addEventListener("mouseleave", onBlockLeave, { passive: true });
639 |
640 | let timer: any;
641 |
642 | function toggleBlockActive(active?: boolean) {
643 | isBlockActive = !!active;
644 | cursorEle &&
645 | (active
646 | ? cursorEle.classList.add("block-active")
647 | : cursorEle.classList.remove("block-active"));
648 | activeDom = active ? node : null;
649 | }
650 |
651 | function onBlockEnter() {
652 | // TODO: maybe control this in other way
653 | cursorEle &&
654 | cursorEle.classList.toggle("lighting--on", !!config.enableLighting);
655 |
656 | // Prevents the cursor from shifting from the node during rapid enter/leave.
657 | toggleNodeTransition(false);
658 |
659 | const rect = node.getBoundingClientRect();
660 | timer && clearTimeout(timer);
661 | toggleBlockActive(true);
662 | // for some edge case, two ele very close
663 | timer = setTimeout(() => toggleBlockActive(true));
664 | cursorEle && cursorEle.classList.add("block-active");
665 | const updateStyleObj: IpadCursorStyle = { ...(config.blockStyle || {}) };
666 | const blockPadding = config.blockPadding ?? 0;
667 | let padding = blockPadding;
668 | let radius = updateStyleObj?.radius;
669 | if (padding === "auto") {
670 | const size = Math.min(rect.width, rect.height);
671 | padding = Math.max(2, Math.floor(size / 25));
672 | }
673 |
674 | if (radius === "auto") {
675 | const paddingCss = Utils.getSize(padding);
676 | const nodeRadius = window.getComputedStyle(node).borderRadius;
677 | if (nodeRadius.startsWith("0") || nodeRadius === "none") radius = "0";
678 | else radius = `calc(${paddingCss} + ${nodeRadius})`;
679 | updateStyleObj.radius = radius;
680 | }
681 |
682 | updateCursorStyle("--cursor-x", `${rect.left + rect.width / 2}px`);
683 | updateCursorStyle("--cursor-y", `${rect.top + rect.height / 2}px`);
684 | updateCursorStyle("--cursor-width", `${rect.width + padding * 2}px`);
685 | updateCursorStyle("--cursor-height", `${rect.height + padding * 2}px`);
686 |
687 | const styleToUpdate: IpadCursorStyle = {
688 | ...updateStyleObj,
689 | ...extractCustomStyle(node),
690 | };
691 |
692 | if (styleToUpdate.durationPosition === undefined) {
693 | styleToUpdate.durationPosition =
694 | styleToUpdate.durationBase ?? config.normalStyle?.durationBase;
695 | }
696 |
697 | updateCursorStyle(Utils.style2Vars(styleToUpdate));
698 |
699 | toggleNodeTransition(true);
700 | node.style.setProperty(
701 | "transform",
702 | "translate(var(--translateX), var(--translateY))"
703 | );
704 | }
705 | function onBlockMove() {
706 | if (!isBlockActive) {
707 | onBlockEnter();
708 | }
709 | const rect = node.getBoundingClientRect();
710 | const halfHeight = rect.height / 2;
711 | const topOffset = (position.y - rect.top - halfHeight) / halfHeight;
712 | const halfWidth = rect.width / 2;
713 | const leftOffset = (position.x - rect.left - halfWidth) / halfWidth;
714 |
715 | const strength = config.adsorptionStrength ?? 10;
716 | updateCursorStyle(
717 | "--cursor-translateX",
718 | `${leftOffset * ((rect.width / 100) * strength)}px`
719 | );
720 | updateCursorStyle(
721 | "--cursor-translateY",
722 | `${topOffset * ((rect.height / 100) * strength)}px`
723 | );
724 |
725 | toggleNodeTransition(false);
726 | const nodeTranslateX = leftOffset * ((rect.width / 100) * strength);
727 | const nodeTranslateY = topOffset * ((rect.height / 100) * strength);
728 | node.style.setProperty("--translateX", `${nodeTranslateX}px`);
729 | node.style.setProperty("--translateY", `${nodeTranslateY}px`);
730 |
731 | // lighting
732 | if (config.enableLighting) {
733 | const lightingSize = Math.max(rect.width, rect.height) * 3 * 1.2;
734 | const lightingOffsetX = position.x - rect.left;
735 | const lightingOffsetY = position.y - rect.top;
736 | updateCursorStyle("--lighting-size", `${lightingSize}px`);
737 | updateCursorStyle("--lighting-offset-x", `${lightingOffsetX}px`);
738 | updateCursorStyle("--lighting-offset-y", `${lightingOffsetY}px`);
739 | }
740 | }
741 | function onBlockLeave() {
742 | timer && clearTimeout(timer);
743 | timer = setTimeout(() => toggleBlockActive(false));
744 | resetCursorStyle();
745 | toggleNodeTransition(true);
746 | node.style.setProperty("transform", "translate(0px, 0px)");
747 | }
748 |
749 | function toggleNodeTransition(enable?: boolean) {
750 | const duration = enable
751 | ? Utils.getDuration(
752 | config?.blockStyle?.durationPosition ??
753 | config?.blockStyle?.durationBase ??
754 | config?.normalStyle?.durationBase ??
755 | "0.23s"
756 | )
757 | : "";
758 | node.style.setProperty(
759 | "transition",
760 | duration ? `all ${duration} cubic-bezier(.58,.09,.46,1.46)` : "none"
761 | );
762 | }
763 |
764 | eventMap.set(node, [
765 | { event: "mouseenter", handler: onBlockEnter },
766 | { event: "mousemove", handler: onBlockMove },
767 | { event: "mouseleave", handler: onBlockLeave },
768 | ]);
769 | }
770 |
771 | function resetCursorStyle() {
772 | if (config.normalStyle?.radius === "auto")
773 | config.normalStyle.radius = config.normalStyle.width;
774 | updateCursorStyle(Utils.style2Vars(config.normalStyle || {}));
775 | }
776 |
777 | function applyTextCursor(sourceNode: HTMLElement) {
778 | updateCursorStyle(Utils.style2Vars(config.textStyle || {}));
779 | const fontSize = window.getComputedStyle(sourceNode).fontSize;
780 | updateCursorStyle("--cursor-font-size", fontSize);
781 | updateCursorStyle(
782 | Utils.style2Vars({
783 | ...config.textStyle,
784 | ...extractCustomStyle(sourceNode),
785 | })
786 | );
787 | }
788 |
789 | /**
790 | * Create custom style that can be bound to `data-cursor-style`
791 | * @param style
792 | */
793 | function customCursorStyle(style: IpadCursorStyle & Record) {
794 | return Object.entries(style)
795 | .map(([key, value]) => `${key}: ${value}`)
796 | .join("; ");
797 | }
798 |
799 | function resetCursor() {
800 | isBlockActive = false;
801 | isTextActive = false;
802 | resetCursorStyle();
803 | }
804 |
805 | const CursorType = {
806 | TEXT: "text" as ICursorType,
807 | BLOCK: "block" as ICursorType,
808 | };
809 |
810 | const exported = {
811 | CursorType,
812 | resetCursor,
813 | initCursor,
814 | updateCursor,
815 | disposeCursor,
816 | updateConfig,
817 | customCursorStyle,
818 | };
819 | export {
820 | CursorType,
821 | resetCursor,
822 | initCursor,
823 | updateCursor,
824 | disposeCursor,
825 | updateConfig,
826 | customCursorStyle,
827 | };
828 | export default exported;
829 |
--------------------------------------------------------------------------------
/src/react/README.md:
--------------------------------------------------------------------------------
1 | ## IPad Cursor React Component
2 |
3 | IPad Cursor support of React component usage.
4 |
5 | ## Usage
6 |
7 | it had exported two function
8 |
9 | 1. `IPadCursorProvider` -> add Context on the Top level code.
10 | ```ts
11 |
12 |
13 |
14 | ```
15 | 2. `useIPadCursor` -> use a Hook to config
16 | ```typescript
17 | const {
18 | updateConfig,
19 | updateCursor,
20 | // ... and so on
21 | } = useIPadCursor();
22 | ```
--------------------------------------------------------------------------------
/src/react/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect, useRef } from 'react'
2 | import type * as CursorOutput from '..'
3 | import {
4 | CursorType,
5 | IpadCursorConfig,
6 | customCursorStyle,
7 | disposeCursor,
8 | initCursor,
9 | resetCursor,
10 | updateConfig,
11 | updateCursor,
12 | } from '..'
13 |
14 | const useIPadCursorInit = (config?: IpadCursorConfig) => {
15 | useLayoutEffect(() => {
16 | initCursor(config)
17 | return () => {
18 | disposeCursor()
19 | }
20 | }, [config])
21 | return null
22 | }
23 |
24 | const CursorContext = React.createContext>({})
25 | export function IPadCursorProvider({
26 | children,
27 | config,
28 | }: {
29 | children: React.ReactNode
30 | config?: IpadCursorConfig
31 | }) {
32 | useIPadCursorInit(config)
33 | return (
34 |
47 | {children}
48 |
49 | )
50 | }
51 |
52 | export function useIPadCursor() {
53 | return React.useContext(CursorContext)
54 | }
55 |
--------------------------------------------------------------------------------
/src/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "declaration": true,
6 | "emitDeclarationOnly": true
7 | }
8 | }
--------------------------------------------------------------------------------
/src/vue/index.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, onUnmounted } from "vue";
2 | import type { App, Directive, Plugin } from "vue";
3 | import {
4 | CursorType,
5 | initCursor,
6 | resetCursor,
7 | updateCursor,
8 | updateConfig,
9 | disposeCursor,
10 | IpadCursorStyle,
11 | IpadCursorConfig,
12 | customCursorStyle,
13 | } from "..";
14 |
15 | export const ipadCursorPlugin: Plugin = {
16 | install(app: App, options?: IpadCursorConfig): any {
17 | initCursor();
18 | options && updateConfig(options);
19 |
20 | function createDirective(type: "block" | "text") {
21 | return {
22 | mounted: (el, binding) => {
23 | el.setAttribute("data-cursor", type);
24 | if (binding.value) {
25 | el.setAttribute(
26 | "data-cursor-style",
27 | typeof binding.value === "string"
28 | ? binding.value
29 | : customCursorStyle(binding.value)
30 | );
31 | }
32 | updateCursor();
33 | },
34 | unmounted: () => updateCursor(),
35 | } as Directive) | string>
36 | }
37 |
38 | app.directive("cursor-block", createDirective('block'));
39 | app.directive("cursor-text", createDirective('text'));
40 | },
41 | };
42 |
43 | export function useCursor(config?: IpadCursorConfig) {
44 | onMounted(() => updateCursor());
45 | onUnmounted(() => updateCursor());
46 | config && updateConfig(config);
47 | initCursor();
48 | return {
49 | CursorType,
50 | resetCursor,
51 | disposeCursor,
52 | initCursor,
53 | updateCursor,
54 | updateConfig,
55 | customCursorStyle,
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "experimentalDecorators": true,
5 | "allowSyntheticDefaultImports": true,
6 | "module": "esnext",
7 | "target": "es2019",
8 | "lib": ["es2019", "dom"],
9 | "strict": true,
10 | "allowJs": false,
11 | "moduleResolution": "node",
12 | "outDir": "dist",
13 | "types": [
14 | "node"
15 | ]
16 | },
17 | "exclude": [
18 | "*.vue",
19 | "playground",
20 | "examples"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/uno.config.ts:
--------------------------------------------------------------------------------
1 | // uno.config.ts
2 | import { defineConfig } from "unocss";
3 |
4 | export default defineConfig({
5 | configFile: "./playground/uno.config.ts",
6 | });
7 |
--------------------------------------------------------------------------------