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.
@@ -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
@@ -0,0 +1,2 @@
1
+ export { useClipboard } from './adapters/react.js';
2
+ export { copyToClipboard, formatData, isSupported } from './core.js';
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
+ }