lite-clipboard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ workflows/publish.yml +52 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/adapters/react.js +49 -0
- package/dist/core.js +20 -0
- package/dist/index.js +2 -0
- package/package.json +75 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Node.js
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '20'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org/'
|
|
22
|
+
|
|
23
|
+
- name: Install pnpm
|
|
24
|
+
uses: pnpm/action-setup@v4
|
|
25
|
+
with:
|
|
26
|
+
version: 9
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: pnpm install
|
|
30
|
+
|
|
31
|
+
# ─── Build ───────────────────────────────────────────
|
|
32
|
+
- name: Build
|
|
33
|
+
run: pnpm build
|
|
34
|
+
|
|
35
|
+
# ─── Quality checks ──────────────────────────────────
|
|
36
|
+
- name: Type check
|
|
37
|
+
run: pnpm test:types
|
|
38
|
+
|
|
39
|
+
- name: Lint
|
|
40
|
+
run: pnpm test:lint
|
|
41
|
+
|
|
42
|
+
- name: Size check
|
|
43
|
+
run: pnpm test:size
|
|
44
|
+
|
|
45
|
+
- name: Tests
|
|
46
|
+
run: pnpm test:coverage
|
|
47
|
+
|
|
48
|
+
# ─── Publish ────────────────────────────────────────
|
|
49
|
+
- name: Publish to npm
|
|
50
|
+
run: npm publish --access public
|
|
51
|
+
env:
|
|
52
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matias Fandiño
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# lite-clipboard
|
|
2
|
+
|
|
3
|
+
A tiny (600 bytes) clipboard library for React/Vue/Svelte
|
|
4
|
+
with a framework-agnostic core. No dependencies. TypeScript native.
|
|
5
|
+
|
|
6
|
+
**Target: <600 bytes gzipped**
|
|
7
|
+
|
|
8
|
+
## Philosophy
|
|
9
|
+
|
|
10
|
+
lite-clipboard is designed as a **framework-agnostic** library:
|
|
11
|
+
|
|
12
|
+
- **Core** (`core.ts`): Pure logic with zero framework imports — works anywhere
|
|
13
|
+
- **Adapters** (`adapters/`): Thin wrapper for each framework (React, Vue, Svelte...)
|
|
14
|
+
- Adding a new framework = one new file in `adapters/`
|
|
15
|
+
|
|
16
|
+
The architecture is inspired by [nanostores](https://github.com/nanostores/nanostores) — the same logic can be wrapped for Vue, Svelte, Solid, or any framework.
|
|
17
|
+
|
|
18
|
+
**Currently supported:** React 18+
|
|
19
|
+
|
|
20
|
+
**Future plans:** Vue composable, Svelte store, Solid signals.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Zero external dependencies
|
|
25
|
+
- Tiny footprint (<600 bytes gzipped)
|
|
26
|
+
- Full TypeScript support
|
|
27
|
+
- Tree-shakeable
|
|
28
|
+
- SSR-safe
|
|
29
|
+
- React hook included
|
|
30
|
+
- Framework-agnostic core (add your own wrapper!)
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
npm install lite-clipboard
|
|
35
|
+
|
|
36
|
+
> lite-clipboard is fully tree-shakeable. Import only what you need
|
|
37
|
+
> and your bundler will eliminate the rest.
|
|
38
|
+
|
|
39
|
+
## Framework support
|
|
40
|
+
|
|
41
|
+
| Framework | Import | Status |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Vanilla JS | `import { copyToClipboard } from 'lite-clipboard'` | ✅ stable |
|
|
44
|
+
| React | `import { useClipboard } from 'lite-clipboard/react'` | ✅ stable |
|
|
45
|
+
| Vue | `import { useClipboard } from 'lite-clipboard/vue'` | 🔜 coming soon |
|
|
46
|
+
| Svelte | `import { useClipboard } from 'lite-clipboard/svelte'` | 🔜 coming soon |
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Default (React)
|
|
51
|
+
|
|
52
|
+
import { useClipboard } from 'lite-clipboard'
|
|
53
|
+
|
|
54
|
+
### Framework-specific (tree-shakeable)
|
|
55
|
+
|
|
56
|
+
import { useClipboard } from 'lite-clipboard/react'
|
|
57
|
+
|
|
58
|
+
### Vanilla JS / Framework agnostic
|
|
59
|
+
|
|
60
|
+
import { copyToClipboard, formatData, isSupported } from 'lite-clipboard'
|
|
61
|
+
|
|
62
|
+
### Core (Framework-Agnostic)
|
|
63
|
+
|
|
64
|
+
The core module works anywhere — browser, Node.js (with clipboard polyfill), or any framework.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { isSupported, copyToClipboard, formatData } from 'lite-clipboard';
|
|
68
|
+
|
|
69
|
+
// Check if clipboard API is available
|
|
70
|
+
if (isSupported()) {
|
|
71
|
+
// Copy string
|
|
72
|
+
await copyToClipboard('Hello!');
|
|
73
|
+
|
|
74
|
+
// Copy JSON (auto-formatted as pretty JSON)
|
|
75
|
+
await copyToClipboard({ type: 'json', value: { name: 'test' } });
|
|
76
|
+
|
|
77
|
+
// Copy code
|
|
78
|
+
await copyToClipboard({ type: 'code', value: 'const x = 1', language: 'js' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Format without copying
|
|
82
|
+
const text = formatData({ type: 'json', value: { foo: 'bar' } });
|
|
83
|
+
// → "{\n \"foo\": \"bar\"\n}"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### React Hook
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { useClipboard } from 'lite-clipboard';
|
|
90
|
+
|
|
91
|
+
function CopyButton({ text }) {
|
|
92
|
+
const { copied, copy } = useClipboard();
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<button onClick={() => copy(text)}>
|
|
96
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
97
|
+
</button>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### With Callbacks
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { useClipboard } from 'lite-clipboard';
|
|
106
|
+
|
|
107
|
+
function CopyButton({ text }) {
|
|
108
|
+
const { copy } = useClipboard({
|
|
109
|
+
onSuccess: () => console.log('Copied!'),
|
|
110
|
+
onError: (err) => console.error('Failed:', err),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return <button onClick={() => copy(text)}>Copy</button>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### With Auto-Reset
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { useClipboard } from 'lite-clipboard';
|
|
121
|
+
|
|
122
|
+
function CopyButton({ text }) {
|
|
123
|
+
// Auto-reset copied state after 2 seconds (default)
|
|
124
|
+
const { copied, copy } = useClipboard({ timeout: 2000 });
|
|
125
|
+
|
|
126
|
+
return <button onClick={() => copy(text)}>{copied ? 'Copied!' : 'Copy'}</button>;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## API
|
|
131
|
+
|
|
132
|
+
### CopyData Type
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
type CopyData =
|
|
136
|
+
| string
|
|
137
|
+
| { type: 'json'; value: unknown }
|
|
138
|
+
| { type: 'code'; value: string; language: string };
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Core Functions
|
|
142
|
+
|
|
143
|
+
| Function | Type | Description |
|
|
144
|
+
|----------|------|-------------|
|
|
145
|
+
| `isSupported()` | `() => boolean` | Check if Clipboard API is available (SSR-safe) |
|
|
146
|
+
| `copyToClipboard(data)` | `(data: CopyData) => Promise<void>` | Copy to clipboard |
|
|
147
|
+
| `formatData(data)` | `(data: CopyData) => string` | Format data without copying |
|
|
148
|
+
| `copy` | alias for `copyToClipboard` | Shorthand |
|
|
149
|
+
|
|
150
|
+
### React Hook
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface UseClipboardOptions {
|
|
154
|
+
timeout?: number; // Auto-reset delay in ms (default: 2000)
|
|
155
|
+
onSuccess?: () => void; // Called on successful copy
|
|
156
|
+
onError?: (error: string) => void; // Called on error
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface UseClipboardReturn {
|
|
160
|
+
copied: boolean; // Current copied state
|
|
161
|
+
error: string | null; // Current error message
|
|
162
|
+
supported: boolean; // Clipboard API availability
|
|
163
|
+
copy: (data: CopyData) => Promise<void>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Bundle Size
|
|
168
|
+
|
|
169
|
+
| Module | Gzipped |
|
|
170
|
+
|--------|---------|
|
|
171
|
+
| Core utilities | ~166 bytes |
|
|
172
|
+
| React hook | ~342 bytes |
|
|
173
|
+
| **Hook target** | **<600 bytes** ✅ |
|
|
174
|
+
|
|
175
|
+
## Browser Support
|
|
176
|
+
|
|
177
|
+
Requires `navigator.clipboard` API:
|
|
178
|
+
|
|
179
|
+
- Chrome
|
|
180
|
+
- Firefox
|
|
181
|
+
- Safari
|
|
182
|
+
- Edge
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
The core is intentionally minimal. Want to add Vue or Svelte support? The core API is designed to be wrapped easily:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Example: Your own Vue composable
|
|
190
|
+
import { copyToClipboard, isSupported } from 'lite-clipboard';
|
|
191
|
+
|
|
192
|
+
export function useClipboard() {
|
|
193
|
+
return {
|
|
194
|
+
copy: copyToClipboard,
|
|
195
|
+
supported: isSupported(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { copyToClipboard, isSupported } from '../core.js';
|
|
3
|
+
export function useClipboard(options = {}) {
|
|
4
|
+
const { onError, onSuccess, timeout = 2000 } = options;
|
|
5
|
+
const supported = isSupported();
|
|
6
|
+
const [copied, setCopied] = useState(false);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const timerRef = useRef(null);
|
|
9
|
+
const handleCopy = useCallback(async (data) => {
|
|
10
|
+
if (timerRef.current) {
|
|
11
|
+
clearTimeout(timerRef.current);
|
|
12
|
+
timerRef.current = null;
|
|
13
|
+
}
|
|
14
|
+
if (!supported) {
|
|
15
|
+
const message = 'Clipboard API not supported';
|
|
16
|
+
setError(message);
|
|
17
|
+
onError?.(message);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
await copyToClipboard(data);
|
|
22
|
+
setCopied(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
onSuccess?.();
|
|
25
|
+
timerRef.current = setTimeout(() => {
|
|
26
|
+
setCopied(false);
|
|
27
|
+
}, timeout);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
31
|
+
setError(message);
|
|
32
|
+
onError?.(message);
|
|
33
|
+
}
|
|
34
|
+
}, [onSuccess, onError, timeout, supported]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
if (timerRef.current) {
|
|
38
|
+
clearTimeout(timerRef.current);
|
|
39
|
+
timerRef.current = null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
return {
|
|
44
|
+
copied,
|
|
45
|
+
copy: handleCopy,
|
|
46
|
+
error,
|
|
47
|
+
supported,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function isSupported() {
|
|
2
|
+
return typeof navigator !== 'undefined' && 'clipboard' in navigator;
|
|
3
|
+
}
|
|
4
|
+
export function formatData(data) {
|
|
5
|
+
if (typeof data === 'string') {
|
|
6
|
+
return data;
|
|
7
|
+
}
|
|
8
|
+
if (data.type === 'json') {
|
|
9
|
+
return JSON.stringify(data.value, null, 2);
|
|
10
|
+
}
|
|
11
|
+
return data.value;
|
|
12
|
+
}
|
|
13
|
+
export async function copyToClipboard(data) {
|
|
14
|
+
if (!isSupported()) {
|
|
15
|
+
throw new Error('Clipboard API not supported');
|
|
16
|
+
}
|
|
17
|
+
const text = formatData(data);
|
|
18
|
+
await navigator.clipboard.writeText(text);
|
|
19
|
+
}
|
|
20
|
+
export const copy = copyToClipboard;
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lite-clipboard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A tiny (600 bytes) clipboard hook for React/Vue/Svelte with a framework-agnostic core.",
|
|
5
|
+
"keywords": ["clipboard", "copy", "react", "hook", "javascript", "typescript"],
|
|
6
|
+
"author": "Matias Fandiño",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": "matifandy8/lite-clipboard",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./react": "./dist/adapters/react.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": "^20.0.0 || >=22.0.0"
|
|
19
|
+
},
|
|
20
|
+
"funding": [
|
|
21
|
+
{
|
|
22
|
+
"type": "github",
|
|
23
|
+
"url": "https://github.com/sponsors/matiffar"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test:lint": "eslint .",
|
|
29
|
+
"test:coverage": "npx tsx core.test.ts && npx tsx adapters/react.test.ts",
|
|
30
|
+
"test:types": "check-dts",
|
|
31
|
+
"test:size": "size-limit",
|
|
32
|
+
"test": "pnpm run /^test:/"
|
|
33
|
+
},
|
|
34
|
+
"prettier": {
|
|
35
|
+
"arrowParens": "avoid",
|
|
36
|
+
"jsxSingleQuote": false,
|
|
37
|
+
"quoteProps": "consistent",
|
|
38
|
+
"semi": false,
|
|
39
|
+
"singleQuote": true,
|
|
40
|
+
"trailingComma": "none"
|
|
41
|
+
},
|
|
42
|
+
"size-limit": [
|
|
43
|
+
{
|
|
44
|
+
"name": "useClipboard",
|
|
45
|
+
"import": { "./dist/index.js": "{ useClipboard }" },
|
|
46
|
+
"limit": "600 B"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "core",
|
|
50
|
+
"import": { "./dist/core.js": "{ copyToClipboard, formatData }" },
|
|
51
|
+
"limit": "300 B"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@logux/eslint-config": "^57.0.2",
|
|
56
|
+
"@size-limit/preset-small-lib": "^12.0.1",
|
|
57
|
+
"@types/node": "^25.5.0",
|
|
58
|
+
"@types/react": "^18.0.0",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
60
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
61
|
+
"better-node-test": "^0.8.3",
|
|
62
|
+
"check-dts": "^0.9.0",
|
|
63
|
+
"clean-publish": "^6.0.3",
|
|
64
|
+
"eslint": "^10.0.3",
|
|
65
|
+
"size-limit": "^12.0.1",
|
|
66
|
+
"tsx": "^4.21.0",
|
|
67
|
+
"typescript": "^5.9.3"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": ">=18"
|
|
71
|
+
},
|
|
72
|
+
"clean-publish": {
|
|
73
|
+
"cleanDocs": true
|
|
74
|
+
}
|
|
75
|
+
}
|